Sommaire

BONJOUR
Musique "Invention BWV784" J.S. BACH 1685-1750

Tutoriel et Help

Style & CSS


Accueil

Accueil CHM


------------------- L'INTOXICATION du siécle -------------------


Le langage C — Systèmes informatiques

Vu dans ... https://sites.uclouvain.be/SystInfo/notes/Theorie/html/C/intro-C.html

Systèmes informatiques

Le langage C

Le langage C¶

Différents langages permettent au programmeur de construire des programmes qui seront exécutés par le processeur. En réalité, le processeur ne comprend qu’un langage : le langage machine. Ce langage est un langage binaire dans lequel toutes les commandes et toutes les données sont représentés sous la forme de séquences de bits.

Le langage machine est peu adapté aux humains et il est extrêmement rare qu’un informaticien doive manipuler des programmes directement en langage machine. Par contre, pour certaines tâches bien spécifiques, comme par exemple le développement de routines spéciales qui doivent être les plus rapides possibles ou qui doivent interagir directement avec le matériel, il est important de pouvoir efficacemment générer du langage machine. Cela peut se faire en utilisant un langage d’assemblage. Chaque famille de processeurs a un langage d’assemblage qui lui est propre. Le langage d’assemblage permet d’exprimer de façon symbolique les différentes instructions qu’un processeur doit exécuter. Nous aurons l’occasion de traiter à plusieurs reprises des exemples en langage d’assemblage dans le cadre de ce cours. Cela nous permettra de mieux comprendre la façon dont le processeur fonctionne et exécute les programmes. Le langage d’assemblage est converti en langage machine grâce à un assembleur.

Le langage d’assemblage est le plus proche du processeur. Il permet d’écrire des programmes compacts et efficaces. C’est aussi souvent la seule façon d’utiliser des instructions spéciales du processeur qui permettent d’interagir directement avec le matériel pour par exemple commander les dispositifs d’entrée/sortie. C’est essentiellement dans les systèmes embarqués qui disposent de peu de mémoire et pour quelques fonctions spécifiques des systèmes d’exploitation que le langage d’assemblage est utilisé de nos jours. La plupart des programmes applicatifs et la grande majorité des systèmes d’exploitation sont écrits dans des langages de plus haut niveau.

Le langage C [KernighanRitchie1998], développé dans les années 70 pour écrire les premières versions du système d’exploitation Unix, est aujourd’hui l’un des langages de programmation les plus utilisés pour développer des programmes qui doivent être rapides ou doivent interagir avec le matériel. La plupart des systèmes d’exploitation sont écrits en langage C.

Le langage C a été conçu à l’origine comme un langage proche du processeur qui peut être facilement compilé, c’est-à-dire traduit en langage machine, tout en conservant de bonnes performances.

La plupart des livres qui abordent la programmation en langage C commencent par présenter un programme très simple qui affiche à l’écran le message Hello, world!.

/**************************************
 * Hello.c
 *
 * Programme affichant sur la sortie
 * standard le message "Hello, world!"
 *
 **************************************/

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
   // affiche sur la sortie standard
   printf("Hello, world!\n");

   return EXIT_SUCCESS;
}

Pour être exécuté, ce programme doit être compilé. Il existe de nombreux compilateurs permettant de transformer le langage C en langage machine. Dans le cadre de ce cours, nous utiliserons gcc(1). Dans certains cas, nous pourrons être amenés à utiliser d’autres compilateurs comme llvm.

La compilation du programme src/hello.c peut s’effectuer comme suit sur une machine de type Unix :

$ gcc -Wall -o hello hello.c
$ ls -l
total 80
-rwxr-xr-x  1 obo  obo  8704 15 jan 22:32 hello
-rw-r--r--  1 obo  obo   288 15 jan 22:32 hello.c

gcc(1) supporte de très nombreuses options et nous aurons l’occasion de discuter de plusieurs d’entre elles dans le cadre de ce cours. Pour cette première utilisation, nous avons choisi l’option -Wall qui force gcc(1) à afficher tous les messages de type warning (dans cet exemple il n’y en a pas) et l’option -o suivie du nom de fichier hello qui indique le nom du fichier dans lequel le programme exécutable doit être sauvegardé par le compilateur [1].

Lorsqu’il est exécuté, le programme hello affiche simplement le message suivant sur la sortie standard :

$ ./hello
Hello, world!
$

Même si ce programme est très simple, il illustre quelques concepts de base en langage C. Tout d’abord comme en Java, les compilateurs récents supportent deux façons d’indiquer des commentaires en C :

  • un commentaire sur une ligne est précédé des caractères //
  • un commentaire qui comprend plusieurs lignes débute par /* et se termine par */

Ensuite, un programme écrit en langage C comprend principalement des expressions en langage C mais également des expressions qui doivent être traduites par le préprocesseur. Lors de la compilation d’un fichier en langage C, le compilateur commence toujours par exécuter le préprocesseur. Celui-ci implémente différentes formes de macros qui permettent notamment d’inclure des fichiers (directives #include), de compiler de façon conditionnelle certaines lignes ou de définir des constantes. Nous verrons différentes utilisations du préprocesseur C dans le cadre de ce cours. À ce stade, les trois principales fonctions du préprocesseur sont :

  • définir des substitutions via la macro #define. Cette macro est très fréquemment utilisée pour définir des constantes ou des substitutions qui sont valables dans l’ensemble du programme.

    #define ZERO  0
#define STRING "SINF1252"

  • importer (directive #include) un fichier. Ce fichier contient généralement des prototypes de fonctions et des constantes. En langage C, ces fichiers qui sont inclus dans un programme sont appelés des header files et ont par convention un nom se terminant par .h. Le programme src/hello.c ci-dessus importe deux fichiers headers standards :

    • <stdio.h> : contient la définition des principales fonctions de la librairie standard permettant l’interaction avec l’entrée et la sortie standard, et notamment printf(3)

    • <stdlib.h>: contient la définition de différentes fonctions et constantes de la librairie standard et notamment EXIT_SUCCESS et EXIT_FAILURE. Ces constantes sont définies en utilisant la macro #define du préprocesseur

      #define EXIT_FAILURE    1
#define EXIT_SUCCESS    0

  • inclure du code sur base de la valeur d’une constante définie par un #define. Ce contrôle de l’inclusion de code sur base de la valeur de constantes est fréquemment utilisé pour ajouter des lignes qui ne doivent être exécutées que lorsque l’on veut débugger un programme. C’est aussi souvent utilisé pour faciliter la portabilité d’un programme entre différentes variantes de Unix, mais cette utilisation sort du cadre de ce cours.

    #define DEBUG
/* ... */
#ifdef DEBUG
printf("debug : ...");
#endif /* DEBUG */

Il est également possible de définir des macros qui prennent un ou plusieurs paramètres [CPP].

Les headers standards sont placés dans des répertoires bien connus du système. Sur la plupart des variantes de Unix ils se trouvent dans le répertoire /usr/include/. Nous aurons l’occasion d’utiliser régulièrement ces fichiers standards dans le cadre du cours.

Le langage Java a été largement inspiré du langage C et de nombreuses constructions syntaxiques sont similaires en Java et en C. Un grand nombre de mots clés en C ont le même rôle qu’en Java. Les principaux types de données primitifs supportés par le C sont :

  • int et long : utilisés lors de la déclaration d’une variable de type entier
  • char : utilisé lors de la déclaration d’une variable permettant de stocker un caractère
  • double et float : utilisés lors de la déclaration d’une variable permettant de stocker un nombre représenté en virgule flottante.

Notez que dans les premières versions du langage C, contrairement à Java, il n’y avait pas de type spécifique permettant de représenter un booléen. Dans de nombreux programmes écrits en C, les booléens sont représentés par des entiers et les valeurs booléennes sont définies [2] comme suit.

#define false   0
#define true    1

Les compilateurs récents qui supportent le type booléen permettent de déclarer des variables de type bool et contiennent les définitions suivantes [2] dans le header standard stdbool.h de [C99].

#define false   (bool)0
#define true    (bool)1

Au-delà des types de données primitifs, Java et C diffèrent et nous aurons l’occasion d’y revenir dans un prochain chapitre. Le langage C n’est pas un langage orienté objet et il n’est donc pas possible de définir d’objet avec des méthodes spécifiques en C. C permet la définition de structures, d’unions et d’énumérations sur lesquelles nous reviendrons.

En Java, les chaînes de caractères sont représentées grâce à l’objet String. En C, une chaîne de caractères est représentée sous la forme d’un tableau de caractères dont le dernier élément contient la valeur \0. Alors que Java stocke les chaînes de caractères dans un objet avec une indication de leur longueur, en C il n’y a pas de longueur explicite pour les chaînes de caractères mais le caractère \0 sert de marqueur de fin de chaîne de caractères. Lorsque le language C a été développé, ce choix semblait pertinent, notamment pour des raisons de performance. Avec le recul, ce choix pose question [Kamp2011] et nous y reviendrons lorsque nous aborderons certains problèmes de sécurité.

  char string[10];
  string[0] = 'j';
  string[1] = 'a';
  string[2] = 'v';
  string[3] = 'a';
  string[4] = '\0';
  printf("String : %s\n", string);

L’exemple ci-dessus illustre l’utilisation d’un tableau de caractères pour stocker une chaîne de caractères. Lors de son exécution, ce fragment de code affiche String : java sur la sortie standard. Le caractère spécial \n indique un passage à la ligne. printf(3) supporte d’autres caractères spéciaux qui sont décrits dans sa page de manuel.

Au niveau des constructions syntaxiques, on retrouve les mêmes boucles et tests en C et en Java :

  • test if (condition)  { ... } else { ... }
  • boucle while (condition) { ... }
  • boucle do { ... } while (condition);
  • boucle for (init; condition; incr) { ... }

En Java, les conditions sont des expressions qui doivent retourner un résultat de type boolean. Le langage C est beaucoup plus permissif puisqu’une condition est une expression qui retourne un nombre entier.

La plupart des expressions et conditions en C s’écrivent de la même façon qu’en Java.

Après ce rapide survol du langage C, revenons à notre programme src/hello.c. Tout programme C doit contenir une fonction nommée main dont la signature [3] est :

int main(int argc, char *argv[])

Lorsque le système d’exploitation exécute un programme C compilé, il démarre son exécution par la fonction main et passe à cette fonction les arguments fournis en ligne de commande [4]. Comme l’utilisateur peut passer un nombre quelconque d’arguments, il faut que le programme puisse déterminer combien d’arguments sont utilisés. En Java, la méthode main a comme signature public static void main(String args[]) et l’attribut args.length permet de connaître le nombres de paramètres passés en arguments d’un programme. En C, le nombre de paramètres est passé dans la variable entière argc et le tableau de chaînes de caractères char *argv[] contient tous les arguments. Le programme src/cmdline.c illustre comment un programme peut accéder à ses arguments.

/**************************************
 * cmdline.c
 *
 * Programme affichant ses arguments
 * sur la sortie standard
 *
 **************************************/

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
  int i;
  printf("Ce programme a %d argument(s)\n", argc);
  for (i = 0; i < argc; i++)
    printf("argument[%d] : %s\n", i, argv[i]);
  return EXIT_SUCCESS;
}

Par convention, en C le premier argument (se trouvant à l’indice 0 du tableau argv) est le nom du programme qui a été exécuté par l’utilisateur. Une exécution de ce programme est illustrée ci-dessous.

Ce programme a 5 argument(s)
argument[0] : ./cmdline
argument[1] : 1
argument[2] : -list
argument[3] : abcdef
argument[4] : sinf1252

Outre le traitement des arguments, une autre différence importante entre Java et C est la valeur de retour de la fonction main. En C, la fonction main retourne un entier. Cette valeur de retour est passée par le système d’exploitation au programme (typiquemment un shell ou interpréteur de commandes) qui a demandé l’exécution du programme. Grâce à cette valeur de retour il est possible à un programme d’indiquer s’il s’est exécuté correctement ou non. Par convention, un programme qui s’exécute sous Unix doit retourner EXIT_SUCCESS lorsqu’il se termine correctement et EXIT_FAILURE en cas d’échec. La plupart des programmes fournis avec un Unix standard respectent cette convention. Dans certains cas, d’autres valeurs de retour non nulles sont utilisées pour fournir plus d’informations sur la raison de l’échec. En pratique, l’échec d’un programme peut être dû aux arguments incorrects fournis par l’utilisateur ou à des fichiers qui sont inaccessibles.

À titre d’illustration, le programme src/failure.c est le programme le plus simple qui échoue lors de son exécution.

/**************************************
 * failure.c
 *
 * Programme minimal qui échoue toujours
 *
 **************************************/

#include <stdlib.h>

int main(int argc, char *argv[])
{
  return EXIT_FAILURE;
}

Enfin, le dernier point à mentionner concernant notre programme src/hello.c est la fonction printf. Cette fonction de la librairie standard se retrouve dans la plupart des programmes écrits en C. Elle permet l’affichage de différentes formes de textes sur la sortie standard. Comme toutes les fonctions de la librairie standard, elle est documentée dans sa page de manuel printf(3). printf(3) prend un nombre variable d’arguments. Le premier argument est une chaîne de caractères qui spécifie le format de la chaîne de caractères à afficher. Une présentation détaillée de printf(3) prendrait de nombreuses pages. À titre d’exemple, voici un petit programme utilisant printf(3)

  char weekday[] = "Monday";
  char month[] = "April";
  int day = 1;
  int hour = 12;
  int min = 42;
  char str[] = "SINF1252";
  int i;

  // affichage de la date et l'heure
  printf("%s, %s %d, %d:%d\n", weekday, month, day, hour, min);

  // affichage de la valeur de PI
  printf("PI = %f\n", 4 * atan(1.0));

  // affichage d'un caractère par ligne
  for(i = 0; str[i] != '\0'; i++)
    printf("%c\n", str[i]);

Lors de son exécution, ce programme affiche :

Monday, April 1, 12:42
PI = 3.141593
S
I
N
F
1
2
5
2

Le langage C permet bien entendu la définition de fonctions. Outre la fonction main qui doit être présente dans tout programme, le langage C permet la définition de fonctions qui retournent ou non une valeur. En C, comme en Java, une fonction de type void ne retourne aucun résultat tandis qu’une fonction de type int retournera un entier. Le programme ci-dessous présente deux fonctions simples. La première, usage ne retourne aucun résultat. Elle affiche un message d’erreur sur la sortie d’erreur standard et termine le programme via exit(2) avec un code de retour indiquant un échec. La seconde, digit prend comme argument un caractère et retourne 1 si c’est un chiffre et 0 sinon. Le code de cette fonction peut paraître bizarre à un programmeur habitué à Java. En C, les char sont représentés par l’entier qui correspond au caractère dans la table des caractères utilisées (voir RFC 20 pour une table ASCII simple). Toutes les tables de caractères placent les chiffres 0 à 9 à des positions consécutives. En outre, en C une expression a priori booléenne comme a < b est définie comme ayant la valeur 1 si elle est vraie et 0 sinon. Il en va de même pour les expressions qui sont combinées en utilisant && ou ||. Enfin, les fonctions getchar(3) et putchar(3) sont des fonctions de la librairie standard qui permettent respectivement de lire (écrire) un caractère sur l’entrée (la sortie) standard.

/**************************************
 * filterdigit.c
 *
 * Programme qui extrait de l'entrée
 * standard les caractères représentant
 * des chiffres
 **************************************/

#include <stdio.h>
#include <stdlib.h>

// retourne vrai si c est un chiffre, faux sinon
// exemple simplifié, voir isdigit dans la librarire standard
// pour une solution complète
int digit(char c)
{
  return ((c >= '0') && (c <= '9'));
}

// affiche un message d'erreur
void usage()
{
  fprintf(stderr, "Ce programme ne prend pas d'argument\n");
  exit(EXIT_FAILURE);
}

int main(int argc, char *argv[])
{
  char c;

  if (argc > 1)
    usage();

  while ((c = getchar()) != EOF) {
    if (digit(c))
        putchar(c);
  }

  return EXIT_SUCCESS;
}

Pages de manuel

Les systèmes d’exploitation de la famille Unix contiennent un grand nombre de librairies, d’appels systèmes et d’utilitaires. Toutes ces fonctions et tous ces programmes sont documentés dans des pages de manuel qui sont accessibles via la commande man. Les pages de manuel sont organisées en 8 sections.

  • Section 1: Utilitaires disponibles pour tous les utilisateurs
  • Section 2: Appels systèmes en C
  • Section 3: Fonctions de la librairie
  • Section 4: Fichiers spéciaux
  • Section 5: Formats de fichiers et conventions pour certains types de fichiers
  • Section 6: Jeux
  • Section 7: Utilitaires de manipulation de fichiers textes
  • Section 8: Commandes et procédure de gestion du système

Dans le cadre de ce cours, nous aborderons principalement les fonctionnalités décrites dans les trois premières sections des pages de manuel. L’accès à une page de manuel se fait via la commande man avec comme argument le nom de la commande concernée. Vous pouvez par exemple obtenir la page de manuel de gcc en tapant man gcc. man supporte plusieurs paramètres qui sont décrits dans sa page de manuel accessible via man man. Dans certains cas, il est nécessaire de spécifier la section du manuel demandée. C’est le cas par exemple pour printf qui existe comme utilitaire (section 1) et comme fonction de la librairie (section 3 - accessible via man 3 printf).

Outre ces pages de manuel locales, il existe également de nombreux sites web où l’on peut accéder aux pages de manuels de différentes versions de Unix dont notamment :

  • les pages de manuel de Debian GNU/Linux
  • les pages de manuel de FreeBSD
  • les pages de manuel de MacOS

Dans la version online de ces notes, toutes les références vers un programme Unix, un appel système ou une fonction de la librairie pointent vers la page de manuel Linux correspondante.

Il existe de nombreux livres consacrés au langage C. La référence la plus classique est [KernighanRitchie1998], mais certains éléments comments à dater. Un tutorial intéressant a été publié par Brian Kernighan [Kernighan]. [King2008] propose une présentation plus moderne du langage C.

Footnotes

[1] Si cette option n’était pas spécifiée, le compilateur aurait placé le programme compilé dans le fichier baptisé a.out.
[2] (1, 2) Formellement, le standard [C99] ne définit pas de type bool mais un type _Bool qui est en pratique renommé en type bool dans la plupart des compilateurs. La définition précise et complète se trouve dans stdbool.h
[3] Il est également possible d’utiliser dans un programme C une fonction main qui ne prend pas d’argument. Sa signature sera alors int main (void).
[4] En pratique, le système d’exploitation passe également les variables d’environnement à la fonction main. Nous verrons plus tard comment ces variables d’environnement sont passées du système au programme et comment celui-ci peut y accéder. Sachez cependant que sous certaines variantes de Unix, et notamment Darwin/MacOS ainsi que sous certaines versions de Windows, le prototype de la fonction main inclut explicitement ces variables d’environnement (int main(int argc, char *argv[], char *envp[]))

© Copyright 2013, O. Bonaventure, G. Detal, C. Paasch. Créé avec Sphinx 1.8.3.


Chapitre 1 : les bases de la programmation en C

1.1 Historique

Le C a été conçu en 1972 par Dennis Richie et Ken Thompson, chercheurs aux Bell Labs, afin de développer un système d'exploitation UNIX sur un DEC PDP-11. En 1978, Brian Kernighan et Dennis Richie publient la définition classique du C dans le livre The C Programming language [6]. Le C devenant de plus en plus populaire dans les années 80, plusieurs groupes mirent sur le marché des compilateurs comportant des extensions particulières. En 1983, l'ANSI (American National Standards Institute) décida de normaliser le langage ; ce travail s'acheva en 1989 par la définition de la norme ANSI C. Celle-ci fut reprise telle quelle par l'ISO (International Standards Organization) en 1990. C'est ce standard, ANSI C, qui est décrit dans le présent document.

1.2 La compilation

Le C est un langage compilé (par opposition aux langages interprétés). Cela signifie qu'un programme C est décrit par un fichier texte, appelé fichier source. Ce fichier n'étant évidemment pas exécutable par le microprocesseur, il faut le traduire en langage machine. Cette opération est effectuée par un programme appelé compilateur. La compilation se décompose en fait en 4 phases successives :
  1. Le traitement par le préprocesseur : le fichier source est analysé par le préprocesseur qui effectue des transformations purement textuelles (remplacement de chaînes de caractères, inclusion d'autres fichiers source ...).
  2. La compilation : la compilation proprement dite traduit le fichier généré par le préprocesseur en assembleur, c'est-à-dire en une suite d'instructions du microprocesseur qui utilisent des mnémoniques rendant la lecture possible.
  3. L'assemblage :cette opération transforme le code assembleur en un fichier binaire, c'est-à-dire en instructions directement compréhensibles par le processeur. Généralement, la compilation et l'assemblage se font dans la foulée, sauf si l'on spécifie explicitement que l'on veut le code assembleur. Le fichier produit par l'assemblage est appelé fichier objet.
  4. L'édition de liens : un programme est souvent séparé en plusieurs fichiers source, pour des raisons de clarté mais aussi parce qu'il fait généralement appel à des librairies de fonctions standard déjà écrites. Une fois chaque code source assemblé, il faut donc lier entre eux les différents fichiers objets. L'édition de liens produit alors un fichier dit exécutable.
Les différents types de fichiers utilisés lors de la compilation sont distingués par leur suffixe. Les fichiers source sont suffixés par .c, les fichiers prétraités par le préprocesseur par .i, les fichiers assembleur par .s, et les fichiers objet par .o. Les fichiers objets correspondant aux librairies pré-compilées ont pour suffixe .a.

Le compilateur C sous UNIX s'appelle cc. On utilisera de préférence le compilateur gccdu projet GNU. Ce compilateur est livré gratuitement avec sa documentation et ses sources. Par défaut, gcc active toutes les étapes de la compilation. On le lance par la commande
gcc [options] fichier.c [-llibrairies]

Par défaut, le fichier exécutable s'appelle a.out. Le nom de l'exécutable peut être modifié à l'aide de l'option -o.

Les éventuelles librairies sont déclarées par la chaîne -llibrairie. Dans ce cas, le système recherche le fichier liblibrairie.a dans le répertoire contenant les librairies pré-compilées (généralement /usr/lib/). Par exemple, pour lier le programme avec la librairie mathématique, on spécifie -lm. Le fichier objet correspondant est libm.a. Lorsque les librairies pré-compilées ne se trouvent pas dans le répertoire usuel, on spécifie leur chemin d'accès par l'option -L.

Les options les plus importantes du compilateur gcc sont les suivantes :
-c : supprime l'édition de liens ; produit un fichier objet.
-E : n'active que le préprocesseur (le résultat est envoyé sur la sortie standard).
-g : produit des informations symboliques nécessaires au débogueur.
-Inom-de-répertoire : spécifie le répertoire dans lequel doivent être recherchés les fichiers en-têtes à inclure (en plus du répertoire courant).
-Lnom-de-répertoire : spécifie le répertoire dans lequel doivent être recherchées les librairies précompilées (en plus du répertoire usuel).
-o nom-de-fichier : spécifie le nom du fichier produit. Par défaut, le exécutable fichier s'appelle a.out.
-O, -O1, -O2, -O3 : options d'optimisations. Sans ces options, le but du compilateur est de minimiser le coût de la compilation. En rajoutant l'une de ces options, le compilateur tente de réduire la taille du code exécutable et le temps d'exécution. Les options correspondent à différents niveaux d'optimisation : -O1 (similaire à -O) correspond à une faible optimisation, -O3 à l'optimisation maximale.
-S : n'active que le préprocesseur et le compilateur ; produit un fichier assembleur.
-v : imprime la liste des commandes exécutées par les différentes étapes de la compilation.
-W : imprime des messages d'avertissement (warning) supplémentaires.
-Wall : imprime tous les messages d'avertissement.
Pour plus de détails sur gcc, on peut consulter le chapitre 4 de [8].

1.3 Les composants élémentaires du C

Un programme en langage C est constitué des six groupes de composants élémentaires suivants :
  • les identificateurs,
  • les mots-clefs,
  • les constantes,
  • les chaînes de caractères,
  • les opérateurs,
  • les signes de ponctuation.
On peut ajouter à ces six groupes les commentaires, qui sont enlevés par le préprocesseur.

1.3.1 Les identificateurs

Le rôle d'un identificateur est de donner un nom à une entité du programme. Plus précisément, un identificateur peut désigner :
  • un nom de variable ou de fonction,
  • un type défini par typedef, struct, union ou enum,
  • une étiquette.
Un identificateur est une suite de caractères parmi :
  • les lettres (minuscules ou majuscules, mais non accentuées),
  • les chiffres,
  • le ``blanc souligné'' (_).
Le premier caractère d'un identificateur ne peut pas être un chiffre. Par exemple, var1, tab_23 ou _deb sont des identificateurs valides ; par contre, 1i et i:j ne le sont pas. Il est cependant déconseillé d'utiliser _ comme premier caractère d'un identificateur car il est souvent employé pour définir les variables globales de l'environnement C.

Les majuscules et minuscules sont différenciées.

Le compilateur peut tronquer les identificateurs au-delà d'une certaine longueur. Cette limite dépend des implémentations, mais elle est toujours supérieure à 31 caractères. (Le standard dit que les identificateurs externes, c'est-à-dire ceux qui sont exportés à l'édition de lien, peuvent être tronqués à 6 caractères, mais tous les compilateurs modernes distinguent au moins 31 caractères).

1.3.2 Les mots-clefs

Un certain nombre de mots, appelés mots-clefs, sont réservés pour le langage lui-même et ne peuvent pas être utilisés comme identificateurs. L'ANSI C compte 32 mots clefs :
auto | const | double | float | int | short | struct | unsigned
break | continue | else | for | long | signed | switch | void
case | default | enum | goto | register | sizeof | typedef | volatile
char | do | extern | if | return | static | union | while
que l'on peut ranger en catégories
  • les spécificateurs de stockage

    auto   register   static   extern   typedef

  • les spécificateurs de type

    char   double   enum   float   int   long   short   signed   struct
    union   unsigned   void

  • les qualificateurs de type

    const   volatile

  • les instructions de contrôle

    break   case   continue   default   do   else   for   goto   if
    switch   while

  • divers

    return   sizeof

1.3.3 Les commentaires

Un commentaire débute par /* et se termine par */. Par exemple,
/*  Ceci est un commentaire */
On ne peut pas imbriquer des commentaires. Quand on met en commentaire un morceau de programme, il faut donc veiller à ce que celui-ci ne contienne pas de commentaire.

1.4 Structure d'un programme C

Une expressionest une suite de composants élémentaires syntaxiquement correcte, par exemple
x = 0
ou bien
(i >= 0) && (i < 10) && (p[i] != 0)
Une instruction est une expression suivie d'un point-virgule. Le point-virgule signifie en quelque sorte ``évaluer cette expression''. Plusieurs instructions peuvent être rassemblées par des accolades { et } pour former une instruction composéeou blocqui est syntaxiquement équivalent à une instruction. Par exemple,
if (x != 0)
{
  z = y / x;
  t = y % x;
}
Une instruction composée d'un spécificateur de type et d'une liste d'identificateurs séparés par une virgule est une déclaration.Par exemple,
int a;
int b = 1, c;
double x = 2.38e4;
char message[80];
En C, toute variable doit faire l'objet d'une déclaration avant d'être utilisée.

Un programme C se présente de la façon suivante :
[directives au préprocesseur]
[déclarations de variables externes]
[fonctions secondaires]

main()
{déclarations de variables internes
  instructions
}
La fonction principale mainpeut avoir des paramètres formels. On supposera dans un premier temps que la fonction main n'a pas de valeur de retour. Ceci est toléré par le compilateur mais produit un message d'avertissement quand on utilise l'option -Wall de gcc (cf. chapitre 4).

Les fonctions secondaires peuvent être placées indifféremment avant ou après la fonction principale. Une fonction secondaire peut se décrire de la manière suivante :
type ma_fonction ( arguments )
{déclarations de variables internes
  instructions
}
Cette fonction retournera un objet dont le type sera type (à l'aide d'une instruction comme return objet;). Les arguments de la fonction obéissent à une syntaxe voisine de celle des déclarations : on met en argument de la fonction une suite d'expressions type objet séparées par des virgules. Par exemple, la fonction secondaire suivante calcule le produit de deux entiers :
int produit(int a, int b)
{
  int resultat;

  resultat = a * b;
  return(resultat);
}

1.5 Les types prédéfinis

Le C est un langage typé. Cela signifie en particulier que toute variable, constante ou fonction est d'un type précis. Le type d'un objet définit la façon dont il est représenté en mémoire.

La mémoire de l'ordinateur se décompose en une suite continue d'octets. Chaque octet de la mémoire est caractérisé par son adresse, qui est un entier. Deux octets contigus en mémoire ont des adresses qui diffèrent d'une unité. Quand une variable est définie, il lui est attribué une adresse. Cette variable correspondra à une zone mémoire dont la longueur (le nombre d'octets) est fixée par le type.

La taille mémoire correspondant aux différents types dépend des compilateurs ; toutefois, la norme ANSI spécifie un certain nombre de contraintes.

Les types de base en C concernent les caractères, les entiers et les flottants (nombres réels). Ils sont désignés par les mots-clefs suivants :

char |
int |
float | double
short | long | unsigned

1.5.1 Le type caractère

Le mot-clef chardésigne un objet de type caractère. Un char peut contenir n'importe quel élément du jeu de caractères de la machine utilisée. La plupart du temps, un objet de type char est codé sur un octet ; c'est l'objet le plus élémentaire en C. Le jeu de caractères utilisé correspond généralement au codage ASCII (sur 7 bits). La plupart des machines utilisent désormais le jeu de caractères ISO-8859 (sur 8 bits), dont les 128 premiers caractères correspondent aux caractères ASCII. Les 128 derniers caractères (codés sur 8 bits) sont utilisés pour les caractères propres aux différentes langues. La version ISO-8859-1 (aussi appelée ISO-LATIN-1) est utilisée pour les langues d'Europe occidentale. Ainsi, le caractère de code 232 est le è, le caractère 233 correspond au é...Pour plus de détails sur l'historique du codage des caractères pour les différentes langues ainsi que sur la norme UNICODE (sur 16 bits, qui permet de coder les caractères pour toutes les langues) et sur la norme ISO/IEC-10646 (sur 32 bits, ce qui permet d'ajouter les caractères anciens), consulter l'article de J. André et M. Goossens [1].

déc. | oct. | hex. | déc. | oct. | hex. | déc. | oct. | hex.
32 40 20 @ 64 100 40 ` 96 140 60
! 33 41 21 A 65 101 41 a 97 141 61
" 34 42 22 B 66 102 42 b 98 142 62
# 35 43 23 C 67 103 43 c 99 143 63
$ 36 44 24 D 68 104 44 d 100 144 64
% 37 45 25 E 69 105 45 e 101 145 65
& 38 46 26 F 70 106 46 f 102 146 66
' 39 47 27 G 71 107 47 g 103 147 67
( 40 50 28 H 72 110 48 h 104 150 68
) 41 51 29 I 73 111 49 i 105 151 69
* 42 52 2a J 74 112 4a j 106 152 6a
+ 43 53 2b K 75 113 4b k 107 153 6b
, 44 54 2c L 76 114 4c l 108 154 6c
- 45 55 2d M 77 115 4d m 109 155 6d
. 46 56 2e N 78 116 4e n 110 156 6e
/ 47 57 2f O 79 117 4f o 111 157 6f
0 | 48 | 60 | 30 | P | 80 | 120 | 50 | p | 112 | 160 | 70
1 | 49 | 61 | 31 | Q | 81 | 121 | 51 | q | 113 | 161 | 71
2 | 50 | 62 | 32 | R | 82 | 122 | 52 | r | 114 | 162 | 72
3 | 51 | 63 | 33 | S | 83 | 123 | 53 | s | 115 | 163 | 73
4 | 52 | 64 | 34 | T | 84 | 124 | 54 | t | 116 | 164 | 74
5 | 53 | 65 | 35 | U | 85 | 125 | 55 | u | 117 | 165 | 75
6 | 54 | 66 | 36 | V | 86 | 126 | 56 | v | 118 | 166 | 76
7 | 55 | 67 | 37 | W | 87 | 127 | 57 | w | 119 | 167 | 77
8 | 56 | 70 | 38 | X | 88 | 130 | 58 | x | 120 | 170 | 78
9 | 57 | 71 | 39 | Y | 89 | 131 | 59 | y | 121 | 171 | 79
: 58 72 3a Z 90 132 5a z 122 172 7a
; 59 73 3b [ 91 133 5b { 123 173 7b
< 60 74 3c \ 92 134 5c | 124 174 7c
= 61 75 3d ] 93 135 5d } 125 175 7d
> 62 76 3e ^ 94 136 5e ~ 126 176 7e
? 63 77 3f _ 95 137 5f DEL 127 177 7f

Table 1.1: Codes ASCII des caractères imprimables
Une des particularités du type char en C est qu'il peut être assimilé à un entier : tout objet de type char peut être utilisé dans une expression qui utilise des objets de type entier. Par exemple, si c est de type char, l'expression c + 1 est valide. Elle désigne le caractère suivant dans le code ASCII. La table précédente donne le code ASCII (en décimal, en octal et en hexadécimal) des caractères imprimables. Ainsi, le programme suivant imprime le caractère 'B'.

main()
{
  char c = 'A';
  printf("%c", c + 1);
}
Suivant les implémentations, le type char est signé ou non. En cas de doute, il vaut mieux préciser unsigned char ou signed char. Notons que tous les caractères imprimables sont positifs.

1.5.2 Les types entiers

Le mot-clef désignant le type entier est int. Un objet de type int est représenté par un mot ``naturel'' de la machine utilisée, 32 bits pour un DEC alpha ou un PC Intel.

Le type int peut être précédé d'un attribut de précision (short ou long) et/ou d'un attribut de représentation (unsigned).Un objet de type short int a au moins la taille d'un char et au plus la taille d'un int. En général, un short int est codé sur 16 bits. Un objet de type long int a au moins la taille d'un int (64 bits sur un DEC alpha, 32 bits sur un PC Intel).

DEC Alpha | PC Intel (Linux)
char | 8 bits | 8 bits | caractère
short | 16 bits | 16 bits | entier court
int | 32 bits | 32 bits | entier
long | 64 bits | 32 bits | entier long
long long | n.i. | 64 bits | entier long (non ANSI)

Table 1.2: Les types entiers
Le bit de poids fort d'un entier est son signe. Un entier positif est donc représenté en mémoire par la suite de 32 bits dont le bit de poids fort vaut 0 et les 31 autres bits correspondent à la décomposition de l'entier en base 2. Par exemple, pour des objets de type char (8 bits), l'entier positif 12 sera représenté en mémoire par 00001100. Un entier négatif est, lui, représenté par une suite de 32 bits dont le bit de poids fort vaut 1 et les 31 autres bits correspondent à la valeur absolue de l'entier représentée suivant la technique dite du ``complément à 2''. Cela signifie que l'on exprime la valeur absolue de l'entier sous forme binaire, que l'on prend le complémentaire bit-à-bit de cette valeur et que l'on ajoute 1 au résultat. Ainsi, pour des objets de type signed char (8 bits), -1 sera représenté par 11111111, -2 par 11111110, -12 par par 11110100.Un int peut donc représenter un entier entre -231 et (231-1). L'attribut unsigned spécifie que l'entier n'a pas de signe. Un unsigned int peut donc représenter un entier entre 0 et (232-1). Sur un DEC alpha, on utilisera donc un des types suivants en fonction de la taille des données à stocker :

signed char | [-27;27[
unsigned char | [0;28[
short int | [-215; 215[
unsigned short int | [0;216[
int | [-231;231[
unsigned int | [0;232[
long int (DEC alpha) | [-263;263[
unsigned long int (DEC alpha) | [0;264[

Plus généralement, les valeurs maximales et minimales des différents types entiers sont définies dans la librairie standard limits.h.

Le mot-clef sizeof a pour syntaxe
sizeof(expression)
où expression est un type ou un objet. Le résultat est un entier égal au nombre d'octets nécessaires pour stocker le type ou l'objet. Par exemple
unsigned short x;

taille = sizeof(unsigned short);
taille = sizeof(x);
Dans les deux cas, taille vaudra 4.

Pour obtenir des programmes portables, on s'efforcera de ne jamais présumer de la taille d'un objet de type entier. On utilisera toujours une des constantes de limits.h ou le résultat obtenu en appliquant l'opérateur sizeof.

1.5.3 Les types flottants

Les types float, double et long double servent à représenter des nombres en virgule flottante. Ils correspondent aux différentes précisions possibles.

DEC Alpha | PC Intel
float | 32 bits | 32 bits | flottant
double | 64 bits | 64 bits | flottant double précision
long double | 64 bits | 128 bits | flottant quadruple précision

Table 1.3: Les types flottants
Les flottants sont généralement stockés en mémoire sous la représentation de la virgule flottante normalisée. On écrit le nombre sous la forme ``signe 0,mantisse Bexposant''. En général, B=2. Le digit de poids fort de la mantisse n'est jamais nul.

Un flottant est donc représenté par une suite de bits dont le bit de poids fort correspond au signe du nombre. Le champ du milieu correspond à la représentation binaire de l'exposant alors que les bits de poids faible servent à représenter la mantisse.

1.6 Les constantes

Une constante est une valeur qui apparaît littéralement dans le code source d'un programme, le type de la constante étant déterminé par la façon dont la constante est écrite. Les constantes peuvent être de 4 types : entier, flottant (nombre réel), caractère, énumération. Ces constantes vont être utilisées, par exemple, pour initialiser une variable.

1.6.1 Les constantes entières

Une constante entière peut être représentée de 3 manières différentes suivant la base dans laquelle elle est écrite :
  • décimale : par exemple, 0 et 2437348 sont des constantes entières décimales.
  • octale : la représentation octale d'un entier correspond à sa décomposition en base 8. Les constantes octales doivent commencer par un zéro. Par exemple, les représentations octales des entiers 0 et 255 sont respectivement 00 et 0377.
  • hexadécimale : la représentation hexadécimale d'un entier correspond à sa décomposition en base 16. Les lettres de a à f sont utilisées pour représenter les nombres de 10 à 15. Les constantes hexadécimales doivent commencer par 0x ou 0X. Par exemple, les représentations hexadécimales de 14 et 255 sont respectivement 0xe et 0xff.
Par défaut, une constante décimale est représentée avec le format interne le plus court permettant de la représenter parmi les formats des types int, long int et unsigned long int tandis qu'une constante octale ou hexadécimale est représentée avec le format interne le plus court permettant encore de la représenter parmi les formats des types int, unsigned int, long int et unsigned long int.

On peut cependant spécifier explicitement le format d'une constante entière en la suffixant par u ou U pour indiquer qu'elle est non signée, ou en la suffixant par l ou L pour indiquer qu'elle est de type long. Par exemple :
constante | type
1234 | int
02322 | int /* octal */
0x4D2 | int /* hexadécimal */
123456789L | long
1234U | unsigned int
123456789UL      | unsigned long int

1.6.2 Les constantes réelles

Les constantes réelles sont représentées par la notation classique par mantisse et exposant. L'exposant est introduit par la lettre e ou E ; il s'agit d'un nombre décimal éventuellement signé.

Par défaut, une constante réelle est représentée avec le format du type double. On peut cependant influer sur la représentation interne de la constante en lui ajoutant un des suffixes f (indifféremment F) ou l (indifféremment L). Les suffixes f et F forcent la représentation de la constante sous forme d'un float, les suffixes l et L forcent la représentation sous forme d'un long double. Par exemple :
constante | type
12.34 | double
12.3e-4 | double
12.34F | float
12.34L | long double

1.6.3 Les constantes caractères

Pour désigner un caractère imprimable, il suffit de le mettre entre apostrophes (par ex. 'A' ou '$'). Les seuls caractères imprimables qu'on ne peut pas représenter de cette façon sont l'antislash et l'apostrophe, qui sont respectivement désignés par \\ et \'. Le point d'interrogation et les guillemets peuvent aussi être désignés par les notations \? et \". Les caractères non imprimables peuvent être désignés par '\code-octal' où code-octal est le code en octal du caractère. On peut aussi écrire '\xcode-hexa' où code-hexa est le code en hexadécimal du caractère (cf. page X). Par exemple, '\33' et '\x1b' désignent le caractère escape. Toutefois, les caractères non-imprimables les plus fréquents disposent aussi d'une notation plus simple :
\n | nouvelle ligne | \r | retour chariot
\t | tabulation horizontale | \f | saut de page
\v | tabulation verticale | \a | signal d'alerte
\b | retour arrière

1.6.4 Les constantes chaînes de caractères

Une chaîne de caractères est une suite de caractères entourés par des guillemets. Par exemple,
"Ceci est une chaîne de caractères"
Une chaîne de caractères peut contenir des caractères non imprimables, désignés par les représentations vues précédemment. Par exemple,
"ligne 1 \n ligne 2"
A l'intérieur d'une chaîne de caractères, le caractère " doit être désigné par \". Enfin, le caractère \ suivi d'un passage à la ligne est ignoré. Cela permet de faire tenir de longues chaînes de caractères sur plusieurs lignes. Par exemple,
"ceci est une longue longue  longue longue longue longue longue longue \
chaîne de caractères"

1.7 Les opérateurs

1.7.1 L'affectation

En C, l'affectation est un opérateur à part entière. Elle est symbolisée par le signe =. Sa syntaxe est la suivante :
variable = expression
Le terme de gauche de l'affectation peut être une variable simple, un élément de tableau mais pas une constante. Cette expression a pour effet d'évaluer expression et d'affecter la valeur obtenue à variable. De plus, cette expression possède une valeur, qui est celle expression. Ainsi, l'expression i = 5 vaut 5.

L'affectation effectue une conversion de type implicite : la valeur de l'expression (terme de droite) est convertie dans le type du terme de gauche.Par exemple, le programme suivant
main()
{
  int i, j = 2;
  float x = 2.5;
  i = j + x;
  x = x + i;
  printf("\n %f \n",x);
}
imprime pour x la valeur 6.5 (et non 7), car dans l'instruction i = j + x;, l'expression j + x a été convertie en entier.

1.7.2 Les opérateurs arithmétiques

Les opérateurs arithmétiques classiques sont l'opérateur unaire - (changement de signe) ainsi que les opérateurs binaires

+ | addition
- | soustraction
* | multiplication
/ | division
% | reste de la division (modulo)

Ces opérateurs agissent de la façon attendue sur les entiers comme sur les flottants. Leurs seules spécificités sont les suivantes :
  • Contrairement à d'autres langages, le C ne dispose que de la notation / pour désigner à la fois la division entière et la division entre flottants. Si les deux opérandes sont de type entier, l'opérateur / produira une division entière (quotient de la division). Par contre, il délivrera une valeur flottante dès que l'un des opérandes est un flottant. Par exemple,
    float x;
x = 3 / 2;
    affecte à x la valeur 1. Par contre
    x = 3 / 2.;
    affecte à x la valeur 1.5.

  • L'opérateur % ne s'applique qu'à des opérandes de type entier. Si l'un des deux opérandes est négatif, le signe du reste dépend de l'implémentation, mais il est en général le même que celui du dividende.
Notons enfin qu'il n'y a pas en C d'opérateur effectuant l'élévation à la puissance. De façon générale, il faut utiliser la fonction pow(x,y) de la librairie math.h pour calculer xy.

1.7.3 Les opérateurs relationnels

> | strictement supérieur
>= | supérieur ou égal
< | strictement inférieur
<= | inférieur ou égal
== | égal
!= | différent

Leur syntaxe est
expression-1 op expression-2
Les deux expressions sont évaluées puis comparées. La valeur rendue est de type int (il n'y a pas de type booléen en C); elle vaut 1 si la condition est vraie, et 0 sinon.

Attention à ne pas confondre l'opérateur de test d'égalité == avec l'opérateur d'affection =. Ainsi, le programme
main()
{
  int a = 0;
  int b = 1;
  if (a = b)
    printf("\n a et b sont egaux \n");
  else
    printf("\n a et b sont differents \n");
}
imprime à l'écran a et b sont egaux !

1.7.4 Les opérateurs logiques booléens

&& | et logique
|| | ou logique
! | négation logique

Comme pour les opérateurs de comparaison, la valeur retournée par ces opérateurs est un int qui vaut 1 si la condition est vraie et 0 sinon.

Dans une expression de type
expression-1 op-1 expression-2 op-2 ...expression-n
l'évaluation se fait de gauche à droite et s'arrête dès que le résultat final est déterminé. Par exemple dans
int i;
int p[10];

if ((i >= 0) && (i <= 9) && !(p[i] == 0))
...
la dernière clause ne sera pas évaluée si i n'est pas entre 0 et 9.

1.7.5 Les opérateurs logiques bit à bit

Les six opérateurs suivants permettent de manipuler des entiers au niveau du bit. Ils s'appliquent aux entiers de toute longueur (short, int ou long), signés ou non.

& | et | | | ou inclusif
^ | ou exclusif | ~ | complément à 1
<< | décalage à gauche | >> | décalage à droite

En pratique, les opérateurs &, | et ~ consistent à appliquer bit à bit les opérations suivantes
& | 0 | 1
0 | 0 | 0
1 | 0 | 1

| | 0 | 1
0 | 0 | 1
1 | 1 | 1

^ | 0 | 1
0 | 0 | 1
1 | 1 | 0
L'opérateur unaire ~ change la valeur de chaque bit d'un entier. Le décalage à droite et à gauche effectuent respectivement une multiplication et une division par une puissance de 2. Notons que ces décalages ne sont pas des décalages circulaires (ce qui dépasse disparaît).

Considérons par exemple les entiers a=77 et b=23 de type unsigned char (i.e. 8 bits). En base 2 il s'écrivent respectivement 01001101 et 00010111.

 | valeur
expression | binaire | décimale
a | 01001101 | 77
b | 00010111 | 23
a & b | 00000101 | 5
a | b | 01011111 | 95
a ^ b | 01011010 | 90
~a | 10110010 | 178
b << 2 | 01011100 | 92 | multiplication par 4
b << 5 | 11100000 | 112 | ce qui dépasse disparaît
b >> 1 | 00001011 | 11 | division entière par 2

1.7.6 Les opérateurs d'affectation composée

Les opérateurs d'affectation composée sont
+=   -=   *=   /=   %=   &=   ^=   |=   <<=   >>=
Pour tout opérateur op, l'expression
expression-1 op= expression-2
est équivalente à
expression-1 = expression-1 op expression-2
Toutefois, avec l'affection composée, expression-1 n'est évaluée qu'une seule fois.

1.7.7 Les opérateurs d'incrémentation et de décrémentation

Les opérateurs d'incrémentation ++ et de décrémentation -- s'utilisent aussi bien en suffixe (i++) qu'en préfixe (++i). Dans les deux cas la variable i sera incrémentée, toutefois dans la notation suffixe la valeur retournée sera l'ancienne valeur de i alors que dans la notation préfixe se sera la nouvelle. Par exemple,
int a = 3, b, c;
b = ++a;      /* a et b valent 4 */
c = b++;      /* c vaut 4 et b vaut 5 */

1.7.8 L'opérateur virgule

Une expression peut être constituée d'une suite d'expressions séparées par des virgules :
expression-1, expression-2, ... , expression-n

Cette expression est alors évaluée de gauche à droite. Sa valeur sera la valeur de l'expression de droite. Par exemple, le programme
main()
{
  int a, b;
  b = ((a = 3), (a + 2));
  printf("\n b = %d \n",b);
}
imprime b = 5.

La virgule séparant les arguments d'une fonction ou les déclarations de variables n'est pas l'opérateur virgule. En particulier l'évaluation de gauche à droite n'est pas garantie. Par exemple l'instruction composée
{
int a=1;
printf("\%d \%d",++a,a);
}
(compilée avec gcc) produira la sortie 2 1 sur un PC Intel/Linux et la sortie 2 2 sur un DEC Alpha/OSF1.

1.7.9 L'opérateur conditionnel ternaire

L'opérateur conditionnel  ? est un opérateur ternaire. Sa syntaxe est la suivante :
condition   ? expression-1 : expression-2
Cette expression est égale à expression-1 si condition est satisfaite, et à expression-2 sinon. Par exemple, l'expression
x >= 0 ? x : -x
correspond à la valeur absolue d'un nombre. De même l'instruction
m = ((a > b) ? a : b);
affecte à m le maximum de a et de b.

1.7.10 L'opérateur de conversion de type

L'opérateur de conversion de type, appelé cast, permet de modifier explicitement le type d'un objet. On écrit
(type) objet
Par exemple,
main()
{
  int i = 3, j = 2;
  printf("%f \n",(float)i/j);
}
retourne la valeur 1.5.

1.7.11 L'opérateur adresse

L'opérateur d'adresse & appliqué à une variable retourne l'adresse-mémoire de cette variable. La syntaxe est
&objet

1.7.12 Règles de priorité des opérateurs

Le tableau suivant classe les opérateurs par ordres de priorité décroissants. Les opérateurs placés sur une même ligne ont même priorité. Si dans une expression figurent plusieurs opérateurs de même priorité, l'ordre d'évaluation est définie par la flèche de la seconde colonne du tableau. On préferera toutefois mettre des parenthèses en cas de doute...

opérateurs
()   []   ->   . ®
!   ~   ++   --   -(unaire)   (type)   *(indirection)   &(adresse) sizeof ¬
*   /   % ®
+   -(binaire) ®
<<   >> ®
<   <=   >   >= ®
==   != ®
&(et bit-à-bit) ®
^ ®
| ®
&& ®
|| ®
? : ¬
=   +=   -=   *=   /=   %=   &=   ^=   |=   <<=   >>= ¬
, ®

Table 1.4: Règles de priorité des opérateurs
Par exemple, les opérateurs logiques bit-à-bit sont moins prioritaires que les opérateurs relationnels. Cela implique que dans des tests sur les bits, il faut parenthéser les expressions. Par exemple, il faut écrire if ((x ^ y) != 0)

1.8 Les instructions de branchement conditionnel

On appelle instruction de contrôle toute instruction qui permet de contrôler le fonctionnement d'un programme. Parmi les instructions de contrôle, on distingue les instructions de branchement et les boucles. Les instructions de branchement permettent de déterminer quelles instructions seront exécutées et dans quel ordre.

1.8.1 Branchement conditionnel if---else

La forme la plus générale est celle-ci :

if (expression-1 )
  instruction-1
else if (expression-2 )
  instruction-2
    ...
else if (expression-n )
  instruction-n
else
  instruction-¥
avec un nombre quelconque de else if ( ... ). Le dernier else est toujours facultatif. La forme la plus simple est

if (expression )
  instruction
Chaque instruction peut être un bloc d'instructions.

1.8.2 Branchement multiple switch

Sa forme la plus générale est celle-ci :
switch (expression )
  {case constante-1:
    liste d'instructions 1
    break;
  case constante-2:
    liste d'instructions 2
    break;
    ...
  case constante-n:
    liste d'instructions n
    break;
  default:
    liste d'instructions ¥
    break;
  }
Si la valeur de expression est égale à l'une des constantes, la liste d'instructions correspondant est exécutée. Sinon la liste d'instructions ¥ correspondant à default est exécutée. L'instruction default est facultative.

1.9 Les boucles

Les boucles permettent de répéter une série d'instructions tant qu'une certaine condition n'est pas vérifiée.

1.9.1 Boucle while

La syntaxe de while est la suivante :

while (expression )
  instruction
Tant que expression est vérifiée (i.e., non nulle), instruction est exécutée. Si expression est nulle au départ, instruction ne sera jamais exécutée. instruction peut évidemment être une instruction composée. Par exemple, le programme suivant imprime les entiers de 1 à 9.
i = 1;
while (i < 10)
  {
    printf("\n i = %d",i);
    i++;
  }

1.9.2 Boucle do---while

Il peut arriver que l'on ne veuille effectuer le test de continuation qu'après avoir exécuté l'instruction. Dans ce cas, on utilise la boucle do---while. Sa syntaxe est

do
  instruction
while (expression );
Ici, instruction sera exécutée tant que expression est non nulle. Cela signifie donc que instruction est toujours exécutée au moins une fois. Par exemple, pour saisir au clavier un entier entre 1 et 10 :
int a;

do
  {
    printf("\n Entrez un entier entre 1 et 10 : ");
    scanf("%d",&a);
  }
while ((a <= 0) || (a > 10));

1.9.3 Boucle for

La syntaxe de for est :

for (expr 1 ;expr 2 ;expr 3)
  instruction
Une version équivalente plus intuitive est :

expr 1;
 while (expr 2 )
   {instruction
    expr 3;
   }
Par exemple, pour imprimer tous les entiers de 0 à 9, on écrit :
for (i = 0; i < 10; i++)
  printf("\n i = %d",i);
A la fin de cette boucle, i vaudra 10. Les trois expressions utilisées dans une boucle for peuvent être constituées de plusieurs expressions séparées par des virgules. Cela permet par exemple de faire plusieurs initialisations à la fois. Par exemple, pour calculer la factorielle d'un entier, on peut écrire :
int n, i, fact;
for (i = 1, fact = 1; i <= n; i++)
  fact *= i;
printf("%d ! = %d \n",n,fact);
On peut également insérer l'instruction fact *= i; dans la boucle for ce qui donne :
int n, i, fact;
for (i = 1, fact = 1; i <= n; fact *= i, i++);
printf("%d ! = %d \n",n,fact);
On évitera toutefois ce type d'acrobaties qui n'apportent rien et rendent le programme difficilement lisible.

1.10 Les instructions de branchement non conditionnel

1.10.1 Branchement non conditionnel break

On a vu le rôle de l'instruction break; au sein d'une instruction de branchement multiple switch. L'instruction break peut, plus généralement, être employée à l'intérieur de n'importe quelle boucle. Elle permet d'interrompre le déroulement de la boucle, et passe à la première instruction qui suit la boucle. En cas de boucles imbriquées, break fait sortir de la boucle la plus interne. Par exemple, le programme suivant :
main()
{
  int i;
  for (i = 0; i < 5; i++)
    {
      printf("i = %d\n",i);
      if (i == 3)
        break;
    }
  printf("valeur de i a la sortie de la boucle = %d\n",i);
}
imprime à l'écran
i = 0
i = 1
i = 2
i = 3
valeur de i a la sortie de la boucle = 3

1.10.2 Branchement non conditionnel continue

L'instruction continue permet de passer directement au tour de boucle suivant, sans exécuter les autres instructions de la boucle. Ainsi le programme
main()
{
  int i;
  for (i = 0; i < 5; i++)
    {
      if (i == 3)
        continue;
      printf("i = %d\n",i);
    }
  printf("valeur de i a la sortie de la boucle = %d\n",i);
}
imprime
i = 0
i = 1
i = 2
i = 4
valeur de i a la sortie de la boucle = 5

1.10.3 Branchement non conditionnel goto

L'instruction goto permet d'effectuer un saut jusqu'à l'instruction etiquette correspondant. Elle est à proscrire de tout programme C digne de ce nom.

1.11 Les fonctions d'entrées-sorties classiques

Il s'agit des fonctions de la librairie standard stdio.h utilisées avec les unités classiques d'entrées-sorties, qui sont respectivement le clavier et l'écran. Sur certains compilateurs, l'appel à la librairie stdio.h par la directive au préprocesseur
#include <stdio.h>
n'est pas nécessaire pour utiliser printf et scanf.

1.11.1 La fonction d'écriture printf

La fonction printf est une fonction d'impression formatée, ce qui signifie que les données sont converties selon le format particulier choisi. Sa syntaxe est
printf("chaîne de contrôle ",expression-1, ..., expression-n);

La chaîne de contrôle contient le texte à afficher et les spécifications de format correspondant à chaque expression de la liste. Les spécifications de format ont pour but d'annoncer le format des données à visualiser. Elles sont introduites par le caractère %, suivi d'un caractère désignant le format d'impression. Les formats d'impression en C sont donnés à la table 1.5.

En plus du caractère donnant le type des données, on peut éventuellemnt préciser certains paramètres du format d'impression, qui sont spécifiés entre le % et le caractère de conversion dans l'ordre suivant :
  • largeur minimale du champ d'impression : %10d spécifie qu'au moins 10 caractères seront réservés pour imprimer l'entier. Par défaut, la donnée sera cadrée à droite du champ. Le signe - avant le format signifie que la donnée sera cadrée à gauche du champ (%-10d).
  • précision : %.12f signifie qu'un flottant sera imprimé avec 12 chiffres après la virgule. De même %10.2f signifie que l'on réserve 12 caractères (incluant le caractère .) pour imprimer le flottant et que 2 d'entre eux sont destinés aux chiffres après la virgule. Lorsque la précision n'est pas spécifiée, elle correspond par défaut à 6 chiffres après la virgule. Pour une chaîne de caractères, la précision correspond au nombre de caractères imprimés : %30.4s signifie que l'on réserve un champ de 30 caractères pour imprimer la chaîne mais que seulement les 4 premiers caractères seront imprimés (suivis de 26 blancs).
format | conversion en | écriture
%d | int | décimale signée
%ld | long int | décimale signée
%u | unsigned int | décimale non signée
%lu | unsigned long int | décimale non signée
%o | unsigned int | octale non signée
%lo | unsigned long int | octale non signée
%x | unsigned int | hexadécimale non signée
%lx | unsigned long int | hexadécimale non signée
%f | double | décimale virgule fixe
%lf | long double | décimale virgule fixe
%e | double | décimale notation exponentielle
%le | long double | décimale notation exponentielle
%g | double | décimale, représentation la plus courte parmi %f et %e
%lg | long double | décimale, représentation la plus courte parmi %lf et %le
%c | unsigned char | caractère
%s | char* | chaîne de caractères

Table 1.5: Formats d'impression pour la fonction printf
Exemple :
#include <stdio.h>
main()
{
  int i = 23674;
  int j = -23674;
  long int k = (1l << 32);
  double x = 1e-8 + 1000;
  char c = 'A';
  char *chaine = "chaine de caracteres";

  printf("impression de i: \n");
  printf("%d \t %u \t %o \t %x",i,i,i,i);
  printf("\nimpression de j: \n");
  printf("%d \t %u \t %o \t %x",j,j,j,j);
  printf("\nimpression de k: \n");
  printf("%d \t  %o \t %x",k,k,k);
  printf("\n%ld \t %lu \t %lo \t %lx",k,k,k,k);
  printf("\nimpression de x: \n");
  printf("%f \t %e \t %g",x,x,x);
  printf("\n%.2f \t %.2e",x,x);
  printf("\n%.20f \t %.20e",x,x);
  printf("\nimpression de c: \n");
  printf("%c \t %d",c,c);
  printf("\nimpression de chaine: \n");
  printf("%s \t %.10s",chaine,chaine);
  printf("\n");
}
Ce programme imprime à l'écran :
impression de i:
23674    23674           56172   5c7a
impression de j:
-23674   4294943622      37777721606     ffffa386
impression de k:
0         0      0
4294967296       4294967296      40000000000     100000000
impression de x:
1000.000000      1.000000e+03    1000
1000.00          1.00e+03
1000.00000001000000000000        1.00000000001000000000e+03
impression de c:
A        65
impression de chaine:
chaine de caracteres     chaine de

1.11.2 La fonction de saisie scanf

La fonction scanf permet de saisir des données au clavier et de les stocker aux adresses spécifiées par les arguments de la fonctions.
scanf("chaîne de contrôle",argument-1,...,argument-n)
La chaîne de contrôle indique le format dans lequel les données lues sont converties. Elle ne contient pas d'autres caractères (notamment pas de \n). Comme pour printf, les conversions de format sont spécifiées par un caractère précédé du signe %. Les formats valides pour la fonction scanf diffèrent légèrement de ceux de la fonction printf.

Les données à entrer au clavier doivent être séparées par des blancs ou des <RETURN> sauf s'il s'agit de caractères. On peut toutefois fixer le nombre de caractères de la donnée à lire. Par exemple %3s pour une chaîne de 3 caractères, %10d pour un entier qui s'étend sur 10 chiffres, signe inclus.

Exemple :
#include <stdio.h>
main()
{
  int i;
  printf("entrez un entier sous forme hexadecimale i = ");
  scanf("%x",&i);
  printf("i = %d\n",i);
}
Si on entre au clavier la valeur 1a, le programme affiche i = 26.

format | type d'objet pointé | représentation de la donnée saisie
%d | int | décimale signée
%hd | short int | décimale signée
%ld | long int | décimale signée
%u | unsigned int | décimale non signée
%hu | unsigned short int | décimale non signée
%lu | unsigned long int | décimale non signée
%o | int | octale
%ho | short int | octale
%lo | long int | octale
%x | int | hexadécimale
%hx | short int | hexadécimale
%lx | long int | hexadécimale
%f | float | flottante virgule fixe
%lf | double | flottante virgule fixe
%Lf | long double | flottante virgule fixe
%e | float | flottante notation exponentielle
%le | double | flottante notation exponentielle
%Le | long double | flottante notation exponentielle
%g | float | flottante virgule fixe ou notation exponentielle
%lg | double | flottante virgule fixe ou notation exponentielle
%Lg | long double | flottante virgule fixe ou notation exponentielle
%c | char | caractère
%s | char* | chaîne de caractères

Table 1.6: Formats de saisie pour la fonction scanf

1.11.3 Impression et lecture de caractères

Les fonctions getchar et putchar permettent respectivement de lire et d'imprimer des caractères. Il s'agit de fonctions d'entrées-sorties non formatées.

La fonction getchar retourne un int correspondant au caractère lu. Pour mettre le caractère lu dans une variable caractere, on écrit
caractere = getchar();
Lorsqu'elle détecte la fin de fichier, elle retourne l'entier EOF (End Of File), valeur définie dans la librairie stdio.h. En général, la constante EOF vaut -1.

La fonction putchar écrit caractere sur la sortie standard :
putchar(caractere);
Elle retourne un int correspondant à l'entier lu ou à la constante EOF en cas d'erreur.

Par exemple, le programme suivant lit un fichier et le recopie caractère par caractère à l'écran.
#include <stdio.h>
main()
{
  char c;

  while ((c = getchar()) != EOF)
    putchar(c);
}
Pour l'exécuter, il suffit d'utiliser l'opérateur de redirection d'Unix :
programme-executable < nom-fichier

Notons que l'expression (c = getchar()) dans le programme précédent a pour valeur la valeur de l'expression getchar() qui est de type int. Le test (c = getchar()) != EOF compare donc bien deux objets de type int (signés).

Ce n'est par contre pas le cas dans le programme suivant :
#include <stdio.h>
main()
{
  char c;
  do
    {
      c = getchar();
      if (c != EOF)
        putchar(c);
    }
  while (c != EOF);
}
Ici, le test c != EOF compare un objet de type char et la constante EOF qui vaut -1. Si le type char est non signé par défaut, cette condition est donc toujours vérifiée. Si le type char est signé, alors le caractère de code 255, y, sera converti en l'entier -1. La rencontre du caractère y sera donc interprétée comme une fin de fichier. Il est donc recommandé de déclarer de type int (et non char) une variable destinée à recevoir un caractère lu par getchar afin de permettre la détection de fin de fichier.

1.12 Les conventions d'écriture d'un programme C

Il existe très peu de contraintes dans l'écriture d'un programme C. Toutefois ne prendre aucune précaution aboutirait à des programmes illisibles. Aussi existe-t-il un certain nombre de conventions.
  • On n'écrit qu'une seule instruction par ligne : le point virgule d'une instruction ou d'une déclaration est toujours le dernier caractère de la ligne.
  • Les instructions sont disposées de telle façon que la structure modulaire du programme soit mise en évidence. En particulier, une accolade ouvrante marquant le début d'un bloc doit être seule sur sa ligne ou placée à la fin d'une ligne. Une accolade fermante est toujours seule sur sa ligne.
  • On laisse un blanc
    • entre les mots-clefs if, while, do, switch et la parenthèse ouvrante qui suit,
    • après une virgule,
    • de part et d'autre d'un opérateur binaire.
  • On ne met pas de blanc entre un opérateur unaire et son opérande, ni entre les deux caractères d'un opérateur d'affectation composée.
  • Les instructions doivent être indentées afin que toutes les instructions d'un même bloc soient alignées. Le mieux est d'utiliser le mode C d'Emacs.

References

[1]
André (J.) et Goossens (M.). -- Codage des caractères et multi-linguisme : de l'Ascii à Unicode et Iso/Iec-10646. Cahiers GUTenberg n-.25em.2ex 20, mai 1985. http://www.gutenberg.eu.org/pub/GUTenberg/publications/cahiers.html.

[2]
Braquelaire (J.-P.). -- Méthodologie de la programmation en C. --
Dunod, 2000, troisième édition.

[3]
Cassagne (B.). -- Introduction au langage C. --
http://clips.imag.fr/commun/bernard.cassagne/Introduction_ANSI_C.html.

[4]
Delannoy (C.). -- Programmer en langage C. -- Eyrolles, 1992.

[5]
Faber (F.). -- Introduction à la programmation en ANSI-C. --
http://www.ltam.lu/Tutoriel_Ansi_C/.

[6]
Kernighan (B.W.) et Richie (D.M.). -- The C programming language. -- Prentice Hall, 1988, seconde édition.

[7]
Léon (L.) et Millet (F.). -- C si facile. -- Eyrolles, 1987.

[8]
Loukides (M.) et Oram (A.). -- Programming with GNU software. -- O'Reilly, 1997.

[9]
Moussel (P.). -- Le langage C. -- http://www.mrc.lu/LangC/.

[10]
Sendrier (N.). -- Notes d' introduction au C. -- Cours de DEA, Université de Limoges, 1998.
This document was translated from LATEX by HEVEA .


Manuel de référence du langage C

1. Introduction

Le présent manuel décrit le langage C tel qu'il est défini dans le projet soumis à l'ANSI le 31 octobre 1988, en vue d'approbation sous le nom Norme nationale américaine pour les systèmes d'information - Langage de programmation C (American National Standard for Information Systems - Programming Language C ), document numéro X3.159-1989. Ce manuel ne constitue qu'une interprétation du projet de norme, et non la norme proprement dite, bien que nous nous soyons efforcés d'en faire un guide fiable du langage.

La plus grande partie de ce manuel suit les grandes lignes du plan de l'avant-projet de norme, qui se fonde lui-même sur celui de la première édition de ce livre, bien que certains détails de l'organisation soient différents. La grammaire du langage proprement dit que nous donnons ici est équivalente à celle de l'avant-projet actuel, mis à part que nous avons modifié les noms de certaines productions, et que nous n'avons pas formalisé les définitions des lexèmes (tokens), ni celles du préprocesseur.

      Tout au long de ce manuel, les commentaires sont mis en retrait et sont de plus petite taille que le texte, comme le présent paragraphe. La plupart du temps, ces commentaires soulignent les différences entre le C de la norme ANSI d'une part, et le langage défini par la prernière édition de ce livre, des perfectionnernents introduits par la suite dans divers compilateurs d'autre part.

2. Les conventions lexicales

Un programme se compose d'une ou plusieurs unités de traduction stockées dans des fichiers. Sa traduction s'effectue en différentes phases, décrites au §12. Les premières phases réalisent des transformations lexicales de bas niveau, exécutent les directives que donnent les lignes commençant par le caractère #, et s'occupent de définir et de développer les macros. A la fin du prétraitement du §12, le programme se réduit à une séquence de lexèmes.

2.1 Les lexèmes

Il existe six sortes de lexèmes : les identificateurs, les mots-clés, les constantes, les constantes de type chaîne, les opérateurs, et les autres séparateurs. Les espaces, les tabulations horizontales et verticales, les caractères de fin de ligne, les caractères de saut de page, ainsi que les commentaires tels que nous les décrivons ci-dessous, s'appellent collectivement les "caractères d'espacement" et sont ignorés, sauf lorsqu'ils séparent des lexèmes. En effet, les caractères d'espacement sont indispensables pour séparer des identificateurs, des mots-clés et des constantes qui seraient adjacents sans eux.

Si le flot d'entrée a été divisé en lexèmes jusqu'à un caractère donné, le lexème suivant est défini comme la plus longue chaîne de caractères pouvant constituer un lexème.

2.2 Les Commentaires

Les caractères /* marquent le début d'un commentaire, qui se termine par les caractères */. Les commentaires ne s'imbriquent pas, et ne figurent pas à l'intérieur de constantes de type chaîne ou caractère.

2.3 Les identificateurs

Un identificateur est une séquence de lettres et de chiffres. Le premier caractère doit être une lettre ; le caractère de soulignement _ compte comme une lettre. Les lettres majuscules et minuscules sont différenciées. Les identificateurs peuvent être de longueur quelconque, et les identificateurs internes peuvent comporter au moins 31 caractères significatifs ; sur certaines implémentations, le nombre de caractères significatifs peut être plus grand. Les identificateurs internes sont les noms de macros du préprocesseur et tous les autres noms n'ayant pas de lien externe (§l1.2). Les identificateurs ayant un lien externe obéissent à des contraintes plus fortes : les implémentations ont le droit de réduire le nombre de caractères significatifs à six, et de ne pas distinguer les majuscules des minuscules.

2.4 Les mots-clés

Les identificateurs suivants sont réservés pour servir de mots-clés ; il ne peuvent servir à rien d'autre ;

        auto      double    int       struct
        break     else      long      switch
        case      enum      register  typedef
        char      extern    return    union
        const     float     short     unsigned
        continue  for       signed    void
        default   goto      sizeof    volatile
        do        if        static    while

Certaines implémentations réservent aussi les mots fortran et asm.

      Les mots-clés const, signed et volatile sont des nouveautés de la norme ANSI ; enum et void ne figuraient pas dans la première édition, mais ils sont employés couramment ; entry, qui était réservé mais n'a jamais été utilisé, ne l'est plus.

2.5 Les constantes

Il existe différentes sortes de constantes, chacune ayant un type particulier ; les types de base sont abordés au §4.2.

    constante :

      constante-entière
      constante caractère
      constante-flottante
      constante-énumérée

2.5.1 Les constantes entières

On considère qu'une constante entière, composée d'une séquence de chiffres, est écrite en octal si elle commence par un 0 (le chiffre zéro), sinon en décimal. Les constantes octales ne peuvent pas contenir les chiffres 8 et 9. Une séquence de chiffres précédée de 0x ou 0x (avec le chiffre zéro) est considérée comme un entier hexadécimal. Les chiffres hexadécimaux vont de a ou A à f ou F, et valent respectivement 10 à 15.

On peut ajouter à une constante entière le suffixe u ou U, pour indiquer qu'elle est non signée (unsigned). On peut également lui ajouter le suffixe l ou L pour indiquer qu'elle est de type long.

Le type d'une constante entière dépend de sa forme, de sa valeur et de son suffixe. (les types sont décrits au §4.) Si elle est décimale et ne comprend pas de suffixe, son type est le premier des types suivants dans laquelle elle peut être représentée : int, long int, unsigned long int. Si elle est octale ou hexadécimale et ne comprend pas de suffixe, son type est le premier des types suivants dans laquelle elle peut être représentée : int, unsigned int, long int, unsigned 1ong int. Si elle comprend le suffixe u ou U, son type est unsigned int ou unsigned long int. Si elle comprend le suffixe 1 ou L, son type est long int ou unsigned long int.

      La détermination des types des constantes entières est beaucoup plus élaborée que dans la première édition, qui se contentait de donner le type long aux grands nombres. Les suffixes u sont nouveaux.

2.5.2 Les constantes de type caractère

Une constante de type caractère est une séquence de un ou plusieurs caractères placés entre apostrophes, comme 'x' . La valeur d'une constante de type caractère ne comprenant qu'un caractère est la valeur numérique de ce caractère dans le jeu de caractères de la machine à l'exécution. La valeur d'une constante à plusieurs caractères dépend de l'implémentation.

Les constantes de type caractère ne peuvent pas contenir le caractère ' ni les caractères de fin de ligne; pour les représenter, ainsi que certains autres caractères, on peut employer les séquences d'échappement suivantes :

    fin de ligne            newline              NL (LF) \n
tabulation horizontale  horizontal tab       HT      \t
tabulation verticale    vertical tab         VT      \v
retour en arrière       backspace            BS      \b
retour chariot          carriage return      CR      \r
saut de page            formfeed             FF      \f
signal sonore           audible alert        BEL     \a
barre oblique           inverse backslash    \       \\
point d'interrogation   question mark        ?       \?
apostrophe              single quote         '       \'
guillemet               double quote         "       \"
nombre octal            octal number         ooo     \ooo
nombre hexadécimal      hexadecimal number   hh      \xhh

La séquence d'échappement \ooo se compose d'une barre oblique inverse suivie de 1, 2 ou 3 chiffres octaux, qui servent à indiquer la valeur du caractère désiré. Un exemple courant de cette construction est \0 (non suivi d'un chiffre), qui représente le caractère NUL. La séquence d'échappement \xhh se compose d'une barre oblique inverse suivie d'un x, puis de chiffres hexadécimaux, qui servent à indiquer la valeur du caractère désiré. Le nombre de chiffres n'est pas limité, mais le résultat est indéfini si la valeur donnée est supérieur à celle du plus grand caractère. Pour les séquences d'échappement en octal ou en hexadécimal, si le type char de l'implémentation est signé, la valeur subit une extension de signe comme dans le cas d'une conversion dans le type char. Si le caractère qui suit le \ n'est pas l'un de ceux énumérés ci-dessus, le résultat est indéfini.

Certaines implémentations comportent un jeu de caractères étendu que l'on ne peut pas représenter dans le type char. Une constante de ce jeu étendu s'écrit avec le préfixe L, par exemple L'x', et s'appelle une constante de type caractère étendu (wide character constant). Une telle constante est de type wchar_t, un type entier défini dans le fichier d'en-tête standard <stddef.h>. Comme pour les constantes de type caractère ordinaires, on peut se servir de séquences d'échappement octales ou hexadécimales ; le résultat est indéfini si la valeur donnée n'est pas représentable dans le type wchar_t.

      Certaines de ces séquences d'échappement sont nouvelles, en particulier la représentation hexadécimale des caractères. Les caractères étendus sont également nouveaux. Les jeux de caractères couramment utilisés dans les deux Amériques et en Europe de l'Ouest peuvent tenir dans le type char par un codage convenable ; on a surtout ajouté wchar_t pour pouvoir traiter les caractètes des langues asiatiques.

2.5.3 Les constantes flottantes

Une constante flottante se compose d'une partie entière, d'un point décimal, d'une partie fractionnaire, d'un e ou d'un E, d'un exposant entier éventuellement signé, et d'un suffixe de type éventuel, à savoir f, F, l ou L. Chacune des parties entière et fractionnaire se compose d'une séquence de chiffres. On peut omettre la partie entière ou la partie fractionnaire (mais pas les deux) ; on peut aussi omettre le point décimal ou le e suivi de l'exposant (mais pas les deux). Le type est déterminé par le suffixe ; F ou f donne un float, L ou l donne un long double ; sinon, le type est double.

      Les suffixes des constantes flottantes sont une nouveauté.

2.5.4 Les constantes énumérées

Les identificateurs déclarés comme énumérateurs (voir §8.4) sont des constantes de type int.

2.6 Les constantes de type chaîne

Une constante de type chaîne, que l'on appelle aussi une constante-chaîne, se compose d'une séquence de caractères placés entre guillemets, comme "...". Une chaîne est de type "tableau de caractères" et de classe de stockage s ta t1c (voir ci-dessous §4), et elle est initialisée avec les caractères donnés. Le fait que des constantes de type chaîne identiques soient distinctes ou non dépend de l'implémentation, et le comportement d'un programme qui essaie de modifier une constante de type chaîne est indéfini.

Les constantes de type chaîne adjacentes sont concaténées en une seule chaîne. Après les concaténations éventuelles, le compilateur ajoute à la chaîne un octet nul \0, afin que les programmes qui parcourent cette chaîne puissent en trouver la fin. Les constantes de type chaîne ne peuvent pas contenir de caractères de fin de ligne ni de guillemets ; pour représenter ces caractères, on peut se servir des mêmes séquences d'échappement que pour les constantes de type caractère.

comme pour les constantes de type caractère, les constantes de type chaîne écrites dans un jeu de caractères étendu sont précédées de L, comme L"...". Les constantes de type chaîne de caractères étendus sont de type "tableau de wchar_t ". La concaténation de constantes de type chaîne de caractères ordinaires et étendus est indéfinie.

      Le fait que les constantes de type chaîne ne soient pas forcément distinctes, et l'interdiction de les modifier, sont des nouveautés de la norme ANSI, de même que la concaténation de constantes de type chaîne. Les constantes de type châtre de caractères étendus sont également nouvelles.

3. La notation syntaxique

Selon la notation syntaxique employée dans cc manuel, les catégories syntaxiques sont imprimées en italique, tandis que les mots et les caractères littéraux sont imprimés dans le style mach ine à é cr ire- Les différentes catégories possibles figurent généralement sur des lignes distinctes ; dans certains cas, une longue série de possibilités est regroupée sur une seule ligne, avec la mention "un parmi". Un symbole facultatif, terminal ou non, porte l'indice "opt", de sorte que, par exemple,

    { expression-opt }

représente une expression facultative, placée entre accolades. Nous récapitulons la syntaxe au §13.

      Contrairement à la grammaire donnée dans la première édition de ce livre, celle que nous décrivons ici explicite les règles de priorité et d'associativité des opérateurs.

4. La signification des identificateurs

Ce que l'on appelle les identificateurs, ou les noms, regroupe en fait plusieurs choses : les fonctions ; les étiquettes de structures, d'unions et d'énumérations ; les membres de structures ou d'unions ; les constantes énumérées ; les noms de définitions de types ; et enfin les objets. Un objet, que l'on appelle parfois une variable, est un emplacement de la mémoire, et son interprétation dépend de deux attributs essentiels : sa classe de stockage et son type. La classe de stockage détermine la durée de vie de la zone mémoire associée à l'objet concerné ; le type détermine la signification des valeurs que l'on trouve dans cet objet Un nom possède aussi une portée, qui est la région du programme où il est connu, et un lien, qui détermine si le même nom dans une autre portée représente le même objet ou la même fonction. Nous abordons les questions de la portée et de l'édition de liens au §11.

4.1 La classe de stockage

Il existe deux classes de stockage : automatique et statique. La classe de stockage d'un objet est précisée par plusieurs mots-clés, ainsi que par le contexte de sa déclaration. Les objets automatiques sont locaux à un bloc (§9.3), et sont effacés lorsqu'on sort de ce bloc. Les déclarations qui figurent à l'intérieur d'un bloc créent des objets automatiques si l'on ne précise pas leur classe de stockage, ou si l'on se sert du spécificateur auto. Les objets déclarés de classe register sont automatiques et sont (si possible) stockés dans les registres rapides de la machine.

Les objets statiques peuvent être locaux à un bloc ou externes à tous les blocs, mais dans les deux cas, ils conservent leur valeur lorsqu'on son des fonctions et des blocs ou quand on y entre à nouveau. A l'intérieur des blocs, y compris les blocs qui contiennent le code d'une fonction, les objets statiques se déclarent grâce au mot-clé static. Les objets déclarés à l'extérieur de tous les blocs, au même niveau que les définitions de fonctions, sont toujours statiques. On peut les rendre locaux à une unité de traduction particulière en ajoutant à leurs déclarations le mot-clé static, ce qui leur donne un lien interne. Ces objets deviennent globaux, utilisables par l'ensemble du programme, si l'on ne précise pas de classe de stockage explicite, ou si l'on se mn du mot-clé extern, ce qui leur donne un lien externe.

4.2 Les types de base

Il existe plusieurs types fondamentaux. Le fichier d'en-tête standard <limits.h>, décrit dans l'annexe B, définit les valeurs extrêmes de chaque type pour l'implémentation utilisée. Les nombres que nous donnons dans l'annexe B représentent les amplitudes minimales acceptables.

Les objets déclarés comme caractères (char) sont assez grands pour mémoriser n'importe quel élément du jeu de caractères d'exécution. Si l'on stocke réellement un caractère de ce jeu dans un objet de type char, sa valeur est celle de l'entier qui code ce caractère, et elle est positive ou nulle. On peut stocker d'autres quantités dans des variables de type char, mais c'est l'implémentation qui détermine quelles sont les valeurs possibles, et surtout le fait que ces valeurs soit signées ou non.

Les caractères non signés, que l'on déclare par unsigned char, prennent la même place que les caractères ordinaires, mais sont toujours positifs ou nuls ; les caractères que l'on déclare explicitement signés, grâce à signed char, prennent également la même place que les caractères ordinaires.

      Le type unsiqned char ne figure pas dans la première édition ce livre, mais il est d'usage courant. signed char est une nouveauté.

En plus des types char, le C fournit jusqu'à trois types d'entiers de tailles différentes, que l'on déclare grâce à short int, int et long int. Les objets de type int tout court comprennent la taille naturelle des nombres pour l'architecture de la machine utilisée ; les autres tailles permettent de répondre à des besoins particuliers. La taille mémoire des entiers longs est au moins égale à celle des entiers courts, mais c'est l'implémentation qui détermine si les entiers énumérés sont longs ou courts. Les types int représentent tous par défaut des valeurs signées.

Les entiers non signés, que l'on déclare grâce au mot-clé unsigned, suivent les lois de l'arithmétique modulo 2^n, où n est le nombre de bits de la représentation utilisée, par conséquent, les calculs sur des quantités non signées n'induisent jamais de dépassement de capacité. L'ensemble des valeurs positives ou nulles que l'on peut stocker dans un objet signé est un sous-ensemble de celles que l'on peut stocker dans l'objet non signé correspondant, et les valeurs communes à ces deux ensembles se représentent de la même manière.

Certains des types en virgule flottante - à savoir la simple précision (float), la double précision (double), et la précision étendue ( long double) - peuvent être équivalents entre eux, mais les derniers de cette liste sont au moins aussi précis que les précédents.

      Le type long double est nouveau. Dans la première édition, long float était équivalents à double ; cette écriture a disparu.

Les énumérations sont des types particuliers qui ont des valeurs entières ; à chaque énumération est associée un ensemble de constantes nommées (§8.4). Les énumérations se comportent comme des entiers, mais les compilateurs donnent souvent des avertissements lorsqu'on affecte à un objet d'une certaine énumération, autre chose que l'une de ses constantes ou une expression de son type.

Comme les objets des types décrits ci-dessus peuvent être considérés comme des nombres, nous les appellerons les types arithmétiques. Les types char et int de toutes tailles, signés ou non, ainsi que les énumérations, seront appelés les types entiers. Les types float, double et long double seront appelés les types flottants.

Le type void représente un ensemble vide de valeurs. Il sert de type de retour aux fonctions qui ne produisent pas de valeur.

4.3 Les types dérivés

En plus des types de base, il existe un ensemble théoriquement infini de types dérivés construits à partir des types fondamentaux grâce aux mécanismes suivants :

  • des tableaux d'objets d'un certain type ;
  • des fonctions retournant des objets d'un certain type ;
  • des pointeurs sur des objets d'un certain type ;
  • des structures contenant une séquence d'objets de types divers ;
  • des unions qui peuvent contenir un objet parmi plusieurs de types divers ;

En général, on peut employer ces méthodes de construction d'objets récursivement.

4.4 Les qualificatifs de type

On peut ajouter différents qualificatifs au type d'un objet. Si l'on déclare un objet const, cela indique que sa valeur ne sera jamais modifiée ; si on le déclare volatile, il aura des propriétés particulières en ce qui concerne l'optimisation. Ces qualificatifs ne modifient pas les valeurs possibles d'un objet ni ses propriétés arithmétiques. Les qualificatifs sont décrits au §8.2.

5. Les objets et les valeurs-g

Un objet est une zone mémoire ponant un nom ; une valeur-g est une expression qui fait référence à un objet. Un exemple trivial d'expression valeur-g est un identificateur ayant le bon type et la bonne classe de stockage. Certains opérateurs produisent des valeurs-g : par exemple, si E est une expression de type pointeur, *E est une expression valeur-g qui fait référence à l'objet pointé par E. Le nom "valeur-g" provient de l'expression d'affectation El = E2, où l'opérande de gauche, El, doit être une expression valeur-g. Dans la description de chaque opérateur, nous précisons s'il a besoin d'une valeur-g et s'il produit une valeur-g.

6. les Conversions

Certains opérateurs peuvent, selon leurs opérandes, provoquer la conversion d'un type à un autre de la valeur d'un opérande. Cette section explique les résultats à attendre de telles conversions. Le §6.5 récapitule les conversions imposées par les opérateurs les plus courants ; le complément à ces informations se trouve dans la description de chaque opérateur.

6.1 La promotion entière

On peut employer un caractère, un entier const, ou un champ de bits entier, signés ou non, ou encore un objet de type énumération, partout où l'on peut employer un entier dans une expression. Si un int peut représenter toutes les valeurs du type d'origine, la valeur est convertie en int ; sinon, elle est convertie en unsigned int. Ce processus s'appelle la promotion entière.

6.2 Les conversions entières

Pour convenir un entier quelconque dans un certain type non signé, on cherche la plus petite valeur positive ou nulle qui soit congrue à cet entier, modulo la plus grande valeur représentable dans ce type non signé plus un. Dans une représentation par complément à deux, ceci équivaut à tronquer la partie gauche de la valeur si le type non signé est plus court (c'est-à-dire s'il se représente sur moins de bits que la valeur à convertir), ou, s'il est plus long, à compléter la valeur par des zéros si elle est non signée, ou par des bits de signe si elle est signée.

Lorsqu'un entier quelconque est converti en un type signé, sa valeur est inchangée si elle est représentable dans ce type ; sinon, elle est définie par l'implémentation.

6.3 Les entiers et les flottants

Lorsqu'une valeur de type flottant est convertie en un type entier, sa partie fractionnaire est effacée ; si la valeur qui en résulte n'est pas représentable dans le type entier, le résultat est indéfini. En particulier, le résultat de la conversion d'une valeur flottante négative en un type entier non signé n'est pas défini.

Lorsqu'un valeur de type entier est convertie en flottant, et si elle n'est pas représentable exactement bien qu'elle soit i à l'intérieur du domaine de représentation du type flottant, le résultat est la valeur représentable la plus proche, soit par défaut, soit par excès. Si la valeur n'est pas dans le domaine de représentation, le résultat est indéfini.

6.4 Les types flottants

Lorsqu'une valeur flottante est convertie en un type flottant plus précis, elle est inchangée. Lorsqu'elle est convertie dans un type flottant moins précis, et si elle est dans le domaine de représentation de ce type, le résultat est la valeur représentable la plus proche, soit par défaut, soit par excès. Si la valeur n'est pas dans le domaine de représentation, le résultat est indéfini.

6.5 Les conversions arithmétiques

De nombreux opérateurs provoquent des convenions et donnent un certain type de résultat selon les mêmes règles. Cela permet de transformer leurs opérandes en un type commun, qui sera celui du résultat. Ces règles s'appellent les conversions arithmétiques usuelles.

  • D'abord, Si l'un des opérandes est de type long double, convertir l'autre en long double.
  • Sinon, si l'un des opérandes est de type double, convertir l'autre en double.
  • Sinon, si l'un des opérandes est de type float, convenir l'autre en float.
  • Sinon, appliquer les promotions entières aux deux opérandes ; puis, si l'un des opérandes est de type unsigned long int, convenir l'autre en unsigned long int.
  • Sinon, si l'un des opérandes est de type long int et l'autre de type unsigned int, le résultat dépend du fait qu'un long int puisse représenter ou non toutes les valeurs d'un unsigned int ; si oui, convenir l'opérande de type unsigned int en long int ; si non, convenir les deux opérandes en unsigned long int.
  • Sinon, si l'un des opérandes est de type long int, convenir l'autre en long int,
  • Sinon, si l'un des opérandes est de type unsigned int, convertir l'autre en unsigned int.
  • Sinon, les deux opérandes sont de type int.

      • Deux changements dans ces règles : premièrement, les calculs sur les opérandes de type float peuvent s'effectuer en simple précision, et non en double ; la première édition stipulait que tous les calculs en virgule flottante s'effectuaient en double précision. Deuxièmement, si l'on combine un type non signé avec un type signé plus long, le résultat est signé ; dans la première édition, la propriété "non signé" était toujours dominante. Ces nouvelles règles sont légèrement plus compliquées, mais elles réduisent nettement les résultats surprenant obtenus auparavant lorsqu'on combinait les quantités signée et non signée. Il est encore possible d'obtenir des résu1ats inattendus si l'on compare une expression non signée à une expression signée de même taille.

6.6 Les pointeurs et les entiers

On peut ajouter ou soustraire une expression de type entier à un pointeur ; dans ce cas, l'expression entière est convertie comme indiqué dans la description de l'opérateur d'addition (§7.7).

On peut soustraire l'un de l'autre deux pointeurs sur des objets de même type contenus dans le même tableau ; le résultat est converti en un entier comme indiqué dans la description de l'opérateur de soustraction (§7.7).

On peut convenir une expression constante entière valant 0, ou une telle expression convertie en void*, en un pointeur de type quelconque, par l'intermédiaire d'une conversion de type, d'une affectation ou d'une comparaison. Le résultat est un pointeur nul égal à un autre pointeur nul du même type, mais différent de tout pointeur sur une fonction ou un objet.

Certaines autres conversions concernant les pointeurs sont autorisées, mais elles dépendent de l'implémentation. Il faut les indiquer par un opérateur de conversion de type explicite (§§7.5 et 8.8).

On peut convertir un pointeur en un type entier assez long pour le contenir ; la taille nécessaire dépend de l'implémentation, ainsi que la fonction de transcodage.

On peut convenir explicitement un objet de type entier en un pointeur. Le transcodage transforme toujours un entier suffisamment long obtenu à partir d'un pointeur donné en ce même pointeur, mais ses autres caractéristiques dépendent de l'implémentation.

On peut convenir un pointeur en un autre pointeur, de même type, aux qualificatifs de l'objet pointé près (§§4.4, 8.2). Si l'on ajoute des qualificatifs, le nouveau pointeur est équivalent à l'ancien, mais il subit les restrictions imposées par les nouveaux qualificatifs. Si l'on en supprime, les opérations effectuées sur l'objet pointé restent soumises aux qualificatifs figurant dans sa déclaration originelle.

Enfin, on peut convertir un pointeur sur une fonction en un pointeur sur un autre type de fonction. Le résultat de l'appel de la fonction pointée par le pointeur converti dépend de l'implémentation ; toutefois, si l'on reconvertit le pointeur dans son type d'origine, il se comporte comme le pointeur original.

6.7 Le type void

On ne peut pas employer n'importe comment la valeur (inexistante) d'un objet de type void (vide), et on ne peut pas lui appliquer de conversion, implicite ou explicite, dans un type autre que void. Puisqu'une expression de type void représente une valeur inexistante, on ne peut s'en servir que si l'on n'a pas besoin de sa valeur, par exemple en tant qu'instruction-expression (§9.2) ou en tant qu'opérande gauche d'un opérateur virgule (§7.18).

On peut convertir une expression dans le type void par une conversion de type explicite. Par exemple, une conversion en void indique que l'on abandonne la valeur d'un appel de fonction servant d'instruction-expression.

      Le type void ne figurait pas dans la première édition de ce livre, mais il était d'usage courant.

6.8 Les pointeurs sur Void

On peut convertir un pointeur quelconque dans le type void * sans perte d'information. Si l'on reconvertit le résultat dans le type d'origine du pointeur, on retrouve le pointeur originel. Contrairement aux conversions de pointeur à pointeur abordées au §6.6, qui nécessitent une conversion de type explicite, on peut réaliser directement des affectations et des comparaisons entre des pointeurs sur des objets et des pointeur de type void*.

      Cette interprétation des pointeurs de type void* est nouvelle ; auparavant c'étaient les pointeurs de type char* qui jouaient le rôle de pointeurs génériques. La norme ANSI autorise explicitement la combinaison de pointeurs sur des objets et de pointeurs de type void*, alors qu'elle impose d'écrire des "casts" explicie pour les autres mélanges de pointeurs.

7. Les expressions

Les grands paragraphes de cette section sont présentés dans l'ordre décroissant des priorités des opérateurs dans les expressions. Ainsi, par exemple, les expressions mentionnées comme opérandes de + (§7.7) sont les expressions définies dans les §§7.1-7.6. A l'intérieur de chaque paragraphe, les opérateurs ont le même degré de priorité. L'associativité à gauche ou à droite de chaque opérateur est précisée dans son paragraphe. La grammaire présentée au §13 spécifie la priorité et l'associativité des opérateurs.

La priorité et l'associativité des opérateurs sont totalement définies, mais l'ordre d'évaluation des expressions ne l'est pas, à quelques exceptions près, même si les sous-expressions entraînent des effets de bord. Par conséquent, sauf si la définition d'un opérateur garantit que ses opérandes sont évalués dans un certain ordre, c'est l'implémentation qui choisit l'ordre d'évaluation des opérandes. Toutefois, chaque opérateur combine les valeurs engendrées par ses opérandes d'une manière compatible avec l'analyse grammaticale de l'expression où il figure.

      Cette régle interdit désormais de modifier l'ordre d'évaluation des expressions comprenant des opérateurs commutatifs et associatifs mathématiquement, mais pas forcément associatifs informatiquement En pratique, ce changement ne conceme que les calculs en virgule flottante aux alentours des limites de précision, et les situations qui peuvent provoquer un dépassement de capacité.

Le traitement des dépassements de capacité, des divisions par zéro et des autres exceptions qui peuvent intervenir au cours de l'évaluation des expressions, n'est pas défini par le langage. La plupart des implémentations du C existantes ne tiennent pas compte des dépassements de capacité pour évaluer les expressions entières signées, ni pour les affectations, mais ce comportement n'est pas garanti. Le traitement de la division par zéro, et de toutes les exceptions en virgule flottante, dépend de l'implémentation ; on peut parfois le contrôler via une bibliothèque de fonctions non standard.

7.1 La génération de pointeurs

Si une expression ou une sous-expression est de type "tableau de T", où T est un type donné, alors la valeur de cette expression est un pointeur sur le premier élément du tableau, et le type de l'expression se transforme en "pointeur sur T". Cette conversion n'a pas lieu si l'expression est l'opérande d'un des opérateurs unaires &, ++, -- ou sizeof, ou l'opérande de gauche d'un opérateur d'affectation ou de l'opérateur ".". De même, une expression de type "fonction retoumant T" est convertie en un "pointeur sur une fonction retournant T", sauf lorsqu'elle sert d'opérande à un opérateur &.

7.2 Les expression primaires

Les expressions primaires sont les identificateurs, les constantes, les chaînes de caractères et les expressions entre parenthèses.

    expression-primaire :

      identificateur
      constante
      constante-caractères
      ( expression )

Un identificateur est une expression primaire, du moment qu'il a été déclaré convenablement, comme indiqué plus loin. Son type est déterminé par sa déclaration. Un identificateur est une valeur-g s'il fait référence à un objet (§5) et s'il est de type arithmétique, structure, union ou pointeur.

Une constante est une expression primaire. Son type dépend de sa forme, comme indiqué au §2.5.

Une constante de type chaîne est une expression primaire, Elle est tout d'abord de type "tableau de char" (ou, pour les chaînes de caractère étendus, "tableau de wchar_t "), mais d'après la règle donnée au §7.1, son type devient généralement Un "pointeur sur char" (ou sur wchar_t), et le résultat est un pointeur sur le premier caractère de la chaîne. Néanmoins, cette conversion n'a pas lieu dans le cas de certains initialisateurs (cf §8.7).

Une expression entre parenthèses est une expression primaire dont le type et la valeur sont identiques à ceux de l'expression intérieure. La présence de parenthèses ne modifie pas le fait que l'expression soit une valeur- g ou non.

7.3 Les expressions postfixées

Les opérateurs des expressions postfixées s'évaluent de gauche à droite.

    expression-postfixée :

      expression-primaire
      expression-postfixée
      [ expression ]
      expression-postfixée ( liste-d'expessions-en-arguments-opt )
      expession-postfixée       . identificateur
      expression-postfixée -> identificateur
      expression-posfixée ++
      expression-postfixée --

    liste-d'expessions-en-arguments :

      expression-d'affectation
      liste-d'expressions-en-arguments       , expression-d'affectation

7.3.1 Les références aux tableaux

Une expression postfixée suivie d'une expression placée entre crochets est une expression postfixée représentant une référence à un tableau indexé, L'une des deux expressions doit être de type "pointeur sur T", où T est un type quelconque, et l'aune doit être de type entier ; le type de l'expression indexée est T, L'expression E1[E2] est équivalente (par définition) à *((E1)+(E2)) , Reportez-vous au §8.6.2 pour de plus amples détails.

7.3.2 Les appels de fonctions

Un appel de fonction se compose d'une expression postfixée, appelée le désignateur de fonction, suivie de parenthèses contenant une liste, éventuellement vide, d'expressions d'affectation séparées par des virgules. (§7.17), qui constituent les arguments de la fonction. Si cette expression postfixée est un identificateur dont il ' n'existe pas de déclaration dans la portée courante, cet identificateur est déclaré implicitement, comme si l'on avait fait figurer la déclaration

    extern int identificateur ();

dans le bloc où se trouve l'appel de fonction. L'expression postfixée (après d'éventuelles déclarations implicites et générations de pointeurs, cf§7.1) doit être de type "pointeur sur une fonction retoumant T", où T est un type quelconque, et la valeur de l'appel de fonction est de type T.

      Dans la première édition, le seul type possible était "fonction", et il fallait employer un opérateur * explicite pour appeler une fonction via un pointeur. La norme ANSI officialise ce que certains compilateurs permettaient déjà, à savoir une syntaxe identique pour les appels de fonctions directs ou via des pointeurs. L'ancienne syntaxe est toujours utilisable.

Le terme argument désigne une expression passée à une fonction lors d'un appel; le terme paramètre désigne un objet (ou son identificateur) reçu par une définition de fonction ou décrit dans une déclaration de fonction. On emploie parfois respectivement les termes "argument (ou paramètre) effectif" et "argument (ou paramètre) formel" pour distinguer ces deux concept.

Lors de la préparation d'un appel de fonction, chaque argument est copié ; tous les passages d'arguments se font par valeur. Une fonction peut modifier les valeurs de ses objets paramètres, qui sont des copies des expressions arguments, mais ces modifications ne peuvent pas toubher les valeurs des arguments. Toutefois, il est possible de passer un pointeur à une fonction, s'il est bien entendu que celle-ci a le droit de modifier la valeur de l'objet pointé par ce pointeur.

On peut déclarer les fonctions sous deux formes différentes. Selon la nouvelle forme, les type des paramètres sont explicites et font partie du type de la fonction ; une telle déclaration s'appelle aussi un prototype de fonction. Selon l'ancienne forme, les types des paramètres ne sont pas précisés. Les déclarations de fonctions sont décrites aux §§8.6.3 et 10.1,

Si, lors d'un appel, la déclaration de fonction dont la portée recouvre cet appel est sous l'ancienne forme, on applique à chaque argument une promotion, de la manière suivante : on applique la promotion entière (§6.1) à chaque argument de type entier, et on convertit chaque argument de type f lo~ t en doub1e. Lc résultat de l'appel est indéfini si le nombre d'arguments est différent du nombre de paramètres figurant dans la définition de la fonction, ou si le type d'un argument, après promotion, est différent du type du paramètre correspondant. La cohérence des types dépend de la forme de la définition de la fonction. Dans le cas de l'ancienne forme, on compare le type de l'argument, après promotion, au type du pararnètre, après promotion ; dans le cas de la nouvelle forme, l'argument, après promotion, doit être de même type que le paramètre, sans promotion.

Si la déclaration de fonction dont la portée recouvre l'appel est sous la nouvelle forme, les arguments sont convertis dans les types des paramètres correspondants du prototype de la fonction, comme pour les affectations. Le nombre d'arguments doit être égal au nombre de paramètres décrits explicites, sauf si liste de paramètres de la déclaration se termine par la notation d'ellipse (,...). Dans ce cas, le nombre d'arguments doit être supérieur ou égal au nombre de paramètres ; les arguments supplémentaires, figurant au-delà des paramètres décrits explicitement, subissent la promotion par défaut des arguments, comme indiqué ci-dessus. Si la fonction est définie sous l'ancienne forme, chaque paramètre du prototype visible lors de l'appel doit être de même type que le paramètre correspondant de la définition, après que celui-ci ait subi la promotion d'argument.

      Ces règles sont particulièrement compliquées à cause du mélange entre les fonctions sous l'ancienne et sous la nouvelle forme. Il vaut mieux éviter de tels mélanges si possible.

L'ordre d'évaluation des arguments n'est pas défini ; notez bien qu'il varie selon les compilateurs. Toutefois, les arguments et le désignateur de fonction sont entièrement évalués, y compris leurs effets de bord, avant d'entrer dans la fonction. Les appels récursifs sont autorisés pour toutes les fonctions.

7.3.3 Les références aux structures

Une expression postfixée suivie d'un point et d'un identificateur est une expression postfixée. Sa première expression opérande doit être une structure ou une union, et l'identificateur doit être le nom d'un membre de cette structure ou de cette union. La valeur de cette expression et son type sont ceux du membre désigné par son nom dans la structure ou l'union. Cette expression est une valeur-g si la dernière expression en est une, et si la seconde n'est pas de type tableau.

Une expression postfixée suivie d'une flèche (formée d'un - et d'un >) et d'un identificateur est une expression postfixée. Sa première expression opérande doit être un pointeur sur une suucture ou une union, et l'identificateur doit être le nom d'un membre de cette structure ou de cette union. Le résultat fait référence au membre désigné par son nom dans la structure ou l'union pointée par l'expression de type pointeur. Le type du résultat est le type de ce membre ; le résultat est une valeur- g s'il n'est pas de type tableau.

Ainsi, l'expression E1->MDS équivaut à (*E1).MDS. Les structures et les unions sonl décrites au §8.3.

      Dans la première édition de ce livre, le nom du membre désigné dans une telle expression devait déjà appartenir à la structure ou l'union donnée dans l'expression postfixée ; toutefois, une note précisait que cette régie n'était pas absolue. Les compilateurs récents, ainsi que la norme ANSI, l'imposent effectivement.

7.3.4 L'incrémentatlon postfixée

Une expression postfixée suivie d'un opérateur ++ ou -- est une expression postfixée. La valeur d'une telle expression est celle de son opémdôe. Après avoir prts note de la valeur, on ajoute (++) ou on retranche (--) 1 à l'opérande (cette opération s'appelle aussi l'incrémentàtion ou la décrémentation, respectivement). L'opérande doit être une valeur-g ; reportez-vous à la description des opérateurs additifs (§7.7) et d'affectation (§7.17) pour connaître les autres contraintes sur l'opérande et les détails de l'opération. Le résultat n'est pas une valeur-g.

7.4 Les opérateurs unaires

Les expressions componant des opérateurs unaires s'évaluent de droite à gauche. ,

    expression-unaire :

      expression-postfixée
      ++ expression-unaire
      -- eprression-unaire
      opérateur-unaire expression-conversion
      sizeof expression-unaire
      sizeof ( nom-de-type )

    opérateurs unaires: un parmi

      & * + - ~!

7.4.1 Les opérateurs d'incrémentation préfixés

Une expression unaire précédée d'un opérateur ++ ou -- est une expression unaire. Son opérande est incrémenté (++) ou décrémenté (--). Sa valeur est prise après l'incrémentation (ou la décrémentation). L'opérande doit être une valeur-g ; reportez-vous à la description des opérateurs additifs (§7.7) et d'affectation (§7.17) pour connaître les autres contraintes sur l'opérande et les détails de l'opération. Le résultat n'est pas une valeur-g.

7.4.2 L'opérateur de prise d'adresse

L'opérateur unaire & prend l'adresse de son opérande, qui doit être soit une valeur-g ne faisant pas référence à un champ de bits ni à un objet de classe register, soit de type fonction. Le résultat est un pointeur sur l'objet ou la fonction désigné par cette valeur-g. Si l'opérande est de type T, le résultat est de type "pointeur sur T".

7.4.3 L'opérateur d'indirection

L'opérateur unaire * indique une indirection, et retoume l'objet ou la fonction pointé par son opérande. C'est une valeur-g si l'opérande est un pointeur sur un objet de type arithmétique, structure, union ou pointeur. Si l'expression est de type "pointeur sur T", le résultat est de type T.

7.4.4 L'opérateur plus unaire

L'opérande de l'opérateur + unaire doit être de type arithmétique, et le résultat est la valeur de cet opérande. Un opérande entier subit la promotion entière. Le résultat est du type de l'opérande après promotion.

      L'opérateur + unaire est une nouveauté de la norme ANSI. Il a été ajouté par souci de symétrie avec le - unaire.

7.4.5 L'opérateur moins unaire

L'opérande de l'opérateur - unaire doit être de type arithmétique, et le résultat est l'opposé de cet opérande. Un opérande entier subit la promotion entière. L'opposé d'une quantité non signée se calcule en soustrayant la valeur après promotion de la plus grande valeur du type résultant de la promotion, et en ajoutant un ; toutefois, l'opposé de zéro est toujours zéro. Le résultat est du type de l'opérande après promotion.

7.4.6 L'opérateur de complément à un

L'opérande de l'opérateur ~ doit être de type entier, et le résultat est le complément à un de cet opérande. L'opérande subit la promotion entière. Si l'opérande est non signé, le résultat se calcule en soustrayant la valeur de la plus grande valeur du type résultant de la promotion. Si l'opérande est signé, le résultat se calcule en convertissant l'opérande après promotion dans le type non signé correspondant, en lui appliquant ~, puis en le reconvertissant dans le type signé. Le résultat est du type de l'opérande après promotion.

7.4.7 L'opérateur de négation logique

L'opérande de l'opérateur ! doit être de type arithmétique ou pointeur, et le résultat vaut 1 si cet opérande vaut 0, et 0 dans tous les autres cas. Le résultat est de type int.

7.4.8 L'opérateur sizeof

L'opérateur sizeof donne le nombre d'octets nécessaires pour mémoriser un objet du type de son opérande. Cet opérande est soit une expression, qui n'est pas évaluée, soit un nom de type entre parenthèses. Lorsqu'on applique s izeof à un char, le résultat vaut 1 ; lorsqu'on l'applique à un tableau, le résultat est le nombre total d'octets qu'occupe ce tableau. Lorsqu'on l'applique à une structure ou à une union, le résultat est le nombre d'octets de cet objet, y compris les octets de remplissage éventuels servant à le superposer à un tableau : la taille d'un tableau de n éléments vaut n fois la taille d'un élément. On ne peut pas appliquer cet opérateur à un opérande de type fonction ou de type incomplet, ni à un champ de bits. Le résultat est une constante entière non signée ; son type particulier dépend de l'implémentation. Le fichier d'en-tête standard <stddef.h> définit ce type et l'appelle size_t.

7.5 L'opérateur "cast" de conversion de type

Une expression unaire précédée d'un nom de type entre parenthèses provoque la conversion de sa valeur en une expression du type indiqué.

    expession-conversion :

      expression-unaire
      ( nom-de-type ) expression-conversion

Cette construction s'appelle une conversion de type (ou cast). Les noms de type sont décrits au §8.8, et les effets des conversions au §6. Une expression comportant une conversion de type n'est pas une valeur-g.

7.6 Les opérateurs multiplicatifs

Les opérateurs multiplicatifs *, / et % s'éva1uent de gauche à droite

    expression-multiplicative :

      expression-conversion
      expression-multiplicative       * expression-conversion
      expression-multiplicative / expression-conversion
      expression-multiplicative % expression-conversion

Les opérandes de * et / doivent être de type arithmétique ; ceux de %, de type entier. Les opérandes subissent les conversions arithmétiques usuelles, qui fixent également le type du résultat.

L'opérateur binaire * indique la multiplication.

L'opérateur binaire / donne le quotient, et l'opérateur % le reste, de la division du premier opérande par le second ; si le second opérande vaut 0, le résultat est indéfini. Sinon, l'expression (a/6)*b+a%b vaut toujours a. Si les deux opérandes sont positifs ou nuls, le reste est positif ou nul, et inférieur au diviseur ; sinon, le langage garantit seulement que la valeur absolue du reste est inférieure à celle du diviseur.

7.7 Les opérateurs additifs

Les opérateurs additifs + et - s'évaluent de gauche à droite. Si les opérandes sont de type arithmétique, ils subissent les conversions arithmétiques usuelles. Chacun de ces opérateurs accepte certains autres types d'opérandes.

    expression-additive :

      expression-multiplicative
      expression-additive       + expression-multiplicative
      expression-additive - expression-multiplicative

L'opérateur + donne la somme de ses opérandes. On peut additionner un pointeur sur un objet d'un tableau et une valeur d'un type entier quelconque. Cette dernière est convertie en un déplacement d'adresse en la multipliant par la taille de l'objet pointé par le pointeur. La somme est alors un pointeur du même type, qui pointe sur un autre objet du même tableau, décalé convenablement par rapport à l'objet de départ. Ainsi, si p pointe sur un certain objet d'un tableau, l'expression p+1 pointe sur l'objet suivant du tableau. Si la somme pointe à l'extérieur des bornes du tableau, le résultat est indéfini, sauf dans le cas de la position suivant immédiatement la borne supérieure.

      La possibilité de pointer un élément plus loin que Ia fin d'un tableau est une nouveauté. Elle rend légitimes des éritures courantes permettant d'exécutcr une boucle sur tous les éléments d'un tableau.

L'opérateur - donne la différence de ses opérandes. On peut soustraire une valeur d'un type entier quelconque d'un pointeur, dans les mêmes conditions que pour l'addition, et avec les mêmes conversions.

Si l'on soustrait deux pointeurs sur des objets de même type, le résultat est une valeur entière signée représentant le décalage entre les objets pointés, étant donné que les pointeurs sur des objets successifs différent de 1. Le type du résultat dépend de l'implémentation, mais il est défini dans le ficher d'en-tête standard <stddef.h> et s'appelle ptrdiff_t. La valeur du résultat est indéfinie si les objets pointés ne font pas partie du même tableau ; toutefois, si p pointe sur le dernier objet d'un tableau, l'expression (p+1)-p vaut 1.

7.8 Les opérateurs de décalage

Les opérateurs de décalage " et " s'évaluent de gauche à droite. Leurs opérandes doivent être de type entier, et subissent les pmmotions entières. Le résultat est du type de l'opérande de gauche, après promotion. Le résultat est indéfini si l'opérande de droite est négatif, ou supérieur ou égal au nombre de bits du type de j'expression de gauche,

    expression-de-décalage :

      expression-additive
      expression-de-décalage       << expression-additive
      expression-de-décalage       >> eexpression-additive

La valeur de E1<<E2 est égale à E1 (vue comme une séquence de bits) décalée de E2 bits vers la gauche ; s'il n'y a pas de dépassement de capacité, cette opération équivaut à une multiplication par 2^n. La valeur de E1>>E2 est égale à E1 (décalée de E2 bits vers la droite ; le décalage à droite équivaut à une division par 2^n si E1 est non signée, ou bien positive ou nulle ; sinon, le résultat dépend de l'implémentation.

7.9 Les opérateurs relationnels

Les opérateurs relationnels s'évaluent de gauche à droite, mais cela n'a pas d'importance ; l'analyse de a<b<c donne (a<b)<c, et a<b vaut soit 0, soit 1.

    expression-relationnelle :

      expression-de-décalage
      expression-relationnelle       < expression-de-décalage
      expression-relationnelle       > expression-de-décalage
      expression-relationnelle       >= expression-de-décalage
      expression-relationnelle       <= expression-de-décalage

Les opérateurs < (inférieur), > (supérieur), <= (inférieur ou égal) et >= (supérieur ou égal) donnent tous 0 si la relation indiquée est fausse, ou bien 1 si elle est vraie. Le résultat est de type int. Les opérandes arithmétiques subissent les conversions arithmétiques usuelles. On peut comparer des pointeurs sur des objets de même type ; le résultat dépend des positions relatives des objets pointés dans l'espace d'adressage.

La comparaison de pointeurs n'est définie que pour des éléments du même objet : si deux pointeurs pointent sur le même objet simple, ils sont égaux ; s'ils pointent sur des membres de la même structure, les plus grands sont ceux qui pointent sur des objets déclarés plus loin dans la structure ; s'ils pointent sur des membres de la même union, ils sont égaux ; s'ils pointent sur des éléments d'un tableau, leur comparaison équivaut à celle des indices correspondants. Si p pointe sur le dernier élément d'un tableau, p+1 est supérieur à p, bien qu'il pointe en dehors du tableau. Dans les autres cas, la comparaison de pointeurs est indéfinie.

      Ces règles assouplissent légèrement les contraintes exposées dans la première édition, en autorisant la comparaison de pointeurs sur des éléments différents d'une structure ou d'une union, ainsi que la comparaison avec un pointeur pointant juste derrière la fin d'un tableau. .

7.10 Les opérateurs d'égalité

    expression-d'égalité :

      expression-relationnelle
      expression-d'égalité       == expression-relationnelle
      expression-d'égalité       != expression-relationnelle

Les opérateurs == (égal à) et != (différent de) sont analogues aux opérateurs relationnels, mis à part que leur degré de priorité est plus faible. (Ainsi, l'expression a<b == c<d vaut 1 si a<b et c<d ont la même valeur logique. ) Les opérateurs d'égalité obéissent aux mêmes règles que les opérateurs relationnels, mais ils offrent d'autres possibilités : on peut comparer un pointeur à une expression en6ère constante valant 0, ou à un pointeur sur vo id (cf §6.6).

7.11 L'opérateur ET bit à bit

    expression-ET :

      expression-d'égalité
      expression-ET       & expression-d'égalité

Les opérandes subissent les conversions arithmétiques usuelles ; le résultat est la combinaison des opérandes par la fonction "ET" appliquée bit par bit. Cet opérateur ne s'applique qu'aux opérandes de type entier.

7.12 L'opérateur OU exclusif bit à bit

    expression-OU-exclusive :

      expression-ET
      expression-OU-exclusive       ^ expression-ET

Les opérandes subissent les conversions arithmétiques usuelles ; le résultat est la combinaison des opérandes par la fonction "OU exclusif" appliquée bit par bit. Cet opérateur ne s'applique qu'aux opérandes de type entier.

7.13 L'opérateur OU inclusif bit à bit

    expression-OU-inclusive :

      expression-OU-exclusive
      expression-OU-inclusive       | expression-OU-exclusive

Les opérandes subissent les conversions arithmétiques usuelles ; le résultat est la combinaison des opérandes par la fonction "OU inclusif" appliquée bit par bit. Cet opérateur ne s'applique qu'aux opérandes de type entier.

7.14 L'opérateur ET logique

    expression-ET-logique :

      expression-OU-inclusive
      expression-ET-logique       && expression-OU-inclusive

L'opérateur && s'évalue de gauche à droite. Il donne 1 si ses deux opérandes sont différents de zéro, ou bien 0 dans les autres cas. Contrairement à l'opérateur &, && garantit que l'évaluation s'effectue de gauche à droite : le premier opérande est évalué, y compris ses effets de bord ; s'il vaut 0, l'expression vaut 0. Sinon, l'opérande de droite est évalué, et l'expression vaut 0 s'il vaut 0, ou 1 dans le cas contraire.

Les opérandes ne sont pas nécessairement du même type, mais ils doivent tous deux être de type arithmétique ou pointeur. Le résultat est de type int.

7.15 L'opérateur OU logique

    expression-OU-logique :

      expression-ET-logique
      expression-OU-logique       || expression-ET-logique

L'opérateur || s'évalue de gauche à droite. Il donne 0 si ses deux opérandes valent zéro, ou bien 1 dans les autres cas. Contrairement à |, || garantit que l'évaluation s'effectue de gauche à droite : le premier opérande est évalué, y compris ses effets de bord ; s'il est différent de 0, l'expression vaut 1. Sinon, l'opérande de droite est évalué, et l'expression vaut 1 s'il est différent de 0, ou 0 dans le cas contraire.

Les opérandes ne sont pas nécessairement du même type, mais ils doivent tous deux être de type arithmétique ou pointeur. Le résultat est de type int.

7.16 L'opérateur conditionnel

    expression-conditionnelle :

      expression-OU-logique
      expression-OU-logique       ? expression : expression-conditionnelle

La première expression est évaluée, y compris ses effets de bord ; si elle est différente de 0, le résultat est la valeur de la deuxième expression, sinon celle de la troisième. on n'évalue qu'un seul opérande parmi le deuxième et le troisième. Si le deuxième et. le troisième opérandes sont arithmétiques, ils subissent les convenions arithmétiques usuelles, ce qui les transforme en un type commun, qui est le type du résultat. Si ces deux opérandes sont de type void, ou bien des structures ou des unions du même type, ou encore des pointeurs sur des objets de même type, le résultat prend ce type commun. Si l'un de ces opérandes est un pointeur et l'autre la constante 0, le 0 est converti dans le type du pointeur, qui devient celui du résultat. Si l'un est un pointeur sur void et l'autre est un autre pointeur, Ce demier est converti en un pointeur sur void, et c'est là le type du résultat.

En ce qui conceme la comparaison de pointeurs, les qualificatifs éventuels (§8.2) du type pointé sont sans importance, mais le type du résultat hérite des qualificatifs venant des deux branches de l'expression conditionnelle.

7.17 Les expressions d'affectation

Il existe plusieurs opérateurs d'affectation ; ils s'évaluent tous de droite à gauche.

    expression-d'affectation :

      expression-conditionnelle
      expression-unaire opérateur-d'affectation expression-d'affectation

    opérateur-d'affectation : un parmi

      = *= /= %= += -= <<= >>= &= ^= |=

Pour tous ces opérateurs, l'opérande de gauche doit être une valeur-g modifiable : il ne peut pas être de type tableau ni de type incomplet, ni être une fonction. De plus, son type ne doit pas porter le qualificatif const ; si c'est une structure ou une union, aucun de ses membres et de ses sous-membres, récursivement, ne doit être qualifié par const. Une expression d'affectation prend le type de son opérande de gauche, et sa valeur après affectation.

Dans le cas de l'affectation simple par =, la valeur de l'expression remplace celle de l'objet désigné par la valeur-g. L'une des conditions suivantes doit être vérifiée : les deux opérandes sont de type arithmétique, auquel cas l'affectation convertit celui de droite dans le type de celui de gauche ; ou bien les deux opérandes sont des structures ou des unions du même type ; ou bien l'un des opérandes est un pointeur est l'autre un pointeur sur void ; ou encore l'opérande de gauche est un pointeur et celui de droite est une expression constante valant 0 ; ou enfin les deux opérandes sont des pointeurs sur des fonctions ou des objets de même type, mis à part que l'opérande de droite peut se passer des qualificatifs const ou volatile caractérisant éventuellement celui de gauche.

Une expression de la forme E1 op= E2 équivaut à E1 = E1 op (E2) , mis à part que E1 n'est évaluée qu'une seule fois.

      D'après les contraintes ci-dessus, il est interdit de réaliser des affectations de pointeurs lorsque la partie droite pointe sur un objet d'un certain type, et la partie gauche sur un objet du type qualifié par const correspondant. Prise à la lettre, cette règle, ainsi que la règle équivalente concernant les conversions de types, entraîne des difficultés pour implémenter oertaines fonctions de la bibliothéque ; cela consstitue une bonne raison pour l'assouplir.

7.18 L'opérateur virgule

    expression :

      expression-d'affectation
      expression       , expression-d'affectation

Deux expressions séparées par une virgule s'évaluent de gauche à droite, et la valeur de l'expression de gauche est perdue. Le résultat prend le type et la valeur de l'opérande de droite. Tous les effets de bord de l'évaluation de l'opérande de gauche sont exécutés avant de commencer à évaluer l'opérande de droite. Dans les contextes où la virgule a un sens spécial, par exemple dans les listes d'arguments de fonctions (§7.3.2) et les listes d'initialisateurs (§8.7), l'unité syntaxique nécessaire est une expression d'affectation, si bien que l'opérateur virgule ne peut figurer que dans un groupement entre parenthèses ; par exemple,

    f(a, (t=3, t+2), c)

a trois arguments et le deuxième d'entre eux vaut 5.

7.19 Les expressions constantes

S yntaxiquement, une expression constante est une expression ne comportant que certains opérateurs :

    expression -constante :

      expression -conditionnelle

Les expressions qui prennent des valeurs constantes sont nécessaires dans divers contextes : après case, en tant que bornes de tableaux et longueurs de champs de bits, comme valeurs de constantes énumérées, dans les initialisateurs et dans certaines expressions du préprocesseur.

Les expressions constantes ne peuvent pas contenir d'affectations, d'opérateurs d'incrémentation ou de décrémentation, d'appels de fonctions, ni d'opérateurs virgule, sauf en tant qu'opérandes de sizeof. Si l'expression constante doit être de type entier, ses opérandes doivent être constitués exclusivement de constantes entières, énumérées, de type caractère, et flottantes ; les conversions de types éventuelles doivent donner des types entiers, et il faut convertir toutes les constantes flottantes en entiers. Ces règles interdisent automatiquement les opérations sur les tableaux et les membres de structure, les indirections et les prises d'adresses. (Toutefois, tous les opérandes sont autorisés dans le cas d'un sizeof.) Les expressions constantes des initialisateurs sont moins restreintes ; leurs opérandes peuvent être des constantes de tout type, et l'on peut y appliquer l'opérateur & unaire à des objets extemes ou statiques, ainsi qu'à des tableaux externes ou statiques indexés par une expression constante. On peut également appliquer l'opérateur & unaire implicitement, en écrivant des tableaux non indexés ou des fonctions. Les valeurs que peut prendre un initialisateur sont d'une part une constante, d'autre part l'adresse d'un objet exteme ou statique déclaré au préalable, plus ou moins une constante.

Les expressions constantes entières qui suivent un #if obéissent à des contraintes plus sévères : les expressions sizeof, les constantes énumérées et les conversions de types y sont interdites (cf §12.5).

8. Les déclarations

Les déclarations indiquent le sens à donner à chaque identificateur ; elles ne réservent pas nécessairement la place mémoire associée à l'identificateur concemé. Les déclarations qui réservent de la mémoire s'appellent des définitiont. Les déclarations sont de la forme -

    déclaration :

      spécificateur-de-déclaration liste-de-déclarateurs-init-opt

Les déclarateurs figurant dans la liste de déclarateurs contiennent les identificateurs à déclarer ; les spécificateurs de déclaration consistent en une séquence de spécificatettrs de classe de stockage et de type.

    spécificateur-de-déclaration :

      spécificateur-de-classe-de-stockage spécificateur-de-déclaration
      spécificateur-de-type spécificateur-de-déclaration-      opt
      qualificatif-de-type spécificateur-de-déclaration-      opt

    liste-de-déclarateurs-init :

      déclarateur-init
      liste-de-déclarateurs-init ,déclarateur-init

    déclarateur-init :

      déclarateur = initialisateur

Nous présenterons les déclarateurs plus loin (§8.5) ; ils contiennent les noms à déclarer. Une déclaration doit comporter au moins un déclarateur, ou bien son spécificaj teur de type doit déclarer une étiquette de structure ou d'union, ou les membres d'une énumération ; les déclarations vides sont interdites.

8.1 Les spécificateurs de classe de stockage

Les spécificatettrs de classe de stockage sont :

    spécificateur-de-classe-de-stockage :

      auto
      register
      static
      extern
      typedef

Le sens des différentes classes de stockage a été exposé au §4.

Les spécificateurs auto et register donnent aux objets déclarés la classe de stockage automatique, et on ne peut les employer qu'à l'intérieur des fonctions. De telles déclarations servent aussi de définitions et réservent de la mémoire. Une déclaration de classe register équivaut à une déclaration de classe auto, mais indique que les objets déclarés seront accédés fréquemment. Peu d'objets sont effectivement placés dans des registres, et seuls certains types sont autorisés ; ces restrictions dépendent de l'implémentation. Dans tous les cas, si un objet est déclaré de classe register, on ne peut pas lui appliquer l'opérateur & unaire, explicitement ou implicitement.

      La rêgle selon laquelle il est interdit de calcu1er l'adresse d'un objet déclaré de classe register, mais étant en fait de classe auto, est nouvelle.

Le spécificateur static donne aux objets déclarés la classe de stockage statique, et on peut l'employer à l'intérieur ou à l'extérieur des fonctions. A l'intérieur d'une fonction, ce spécificateur réserve de la mémoire, et sert de définition ; son effet à l'extérieur d'une fonction est décrit au §11.2.

Une déclaration comportant le spécificateur extern, et figurant à l'intérieur d'une fonction, indique que l'espace mémoire réservé aux objets déclarés est défini ailleurs ; son effet à l'extérieur d'une fonction est décrit au §11.2.

Le spécificateur typedef ne réserve pas de mémoire, et c'est seulement par commodité syntaxique qu'il figure parmi les spécificateurs de classe de stockage ; il est décrit au §8.9.

On ne peut faire figurer qu'un seul spécificateur de classe de stockage par déclaration. Si l'on n'en donne pas, les règles suivantes s'appliquent : les objets déclarés à l'intérieur d'une fonction sont de classe auto ; les fonctions déclarées à l'intérieur d'une fonction sont de classe extern ; les objets et les fonctions déclarés à l'extérieur des fonctions sont de classe statique, avec lien externe (cf § §10-Al1).

8.2 Les spécificateurs de type

Les spécificateurs de type sont :

    spécificateur-de-type :

      void
      char
      short
      int
      long
      float
      double
      signed
      unsigned
      spécificareur-de-struct-ou-union
      spécificateur-d'énumération
      nom-typedef

On peut ajouter l'un des mots short ou long à int ; le sens est le même si l'on omet int. On peut également ajouter le mot long à double. On peut ajouter l'un des mots signed ou unsigned à int, à ses variantes short ou long, ou bien à char. Si l'on n'écrit que signed ou unsigned, cela représente un int. Le spécificateur signed sert à forcer les objets de type char à porter un signe ; pour les autres types entiers, signed est autorisé, mais redondant.

Mis à part les cas ci-dessus, on ne peut faire figurer qu'un seul spécificateur de type par déclaration. Si l'on n'en donne pas, le type est int.

On peut aussi qualifier les types, afin de donner des propriétés particulières aux objets déclarés.

    qualificatif-de-type:

      const
      volatile

On peut ajouter des qualificatifs de type à n'importe quel spécificateur de type. On peut initialiser un objet const, mais on ne peut rien lui affecter par la suite. Le sens d'un objet volatile dépend de l'implémentation.

      Les propriétés const et volatile sont des nouveautés de la norme ANSI. const permet au compilateur de connaître les objets qui peuvent étrc placés en mémoire morte, et lui donne parfois de meilleures possibilités d'optimisation. Le qualificatif volatile sert à forcer une implémentation donnée à ne pas réaliser d'optimisation indésirable. Par exemple, dans le cas d'une machine dont les registres d'entrée-sorties se situent à des adresses de Ia mémoire centrale, on peut déclarer un pointeur sur un registre de périphérique comme pointeur sur volatile, afin que le compilateur ne supprime pas les indirections via ce pointcur, apparemment redondantes. les compilateurs peuvent ne pas tenir compte de ces qualificatifs, mis à part qu'ils doivent signaler les tentatives explicites de modification des objets const.

8.3 Les déclarations de structures et d'unions

Une structure est un objet composé d'une séquence de membres de types divers, portant des noms. Une union est un objet qui contient, selon les moments, l'un de ses membres, qui sont de types divers. Les spécificateurs de structures et d'unions sont de la même forme.

    spécificateur-de-struct-ou-union :

      struct-ou-union identificateur-opt { liste-de-déclarartions-de-struct}
      struct-ou-union identificateur

    struct-ou-union :

      struct
      union

Une liste-de-déclarations-de-struct est une séquence de déclarations des membres de la structure ou de l'union :

    liste-de-déclarations-de-struct :

      déclaration-de-struct
      liste-de-déclarations-de-struct déclaration-de-struct

    déclaration-de-struct :

      liste-de-qualificatifs-de-spécificateurs liste-de-déclarateurs-de-struct ;

    liste-de-qualificatifs-de-spécificateurs :

      spécificateus-de-type liste-de-qualificatifs-de-spécificateurs-opt
      qualificatif-de-type liste-de-qualificatifs-de-spécificateurs-opt

    liste-de-déclarateurs-de-struct :

      déclarareur-de-struct
      liste-de-déclarateurs-de-struct       , déclarateur-de-struct

En général, un déclarateur-de-struct est simplement le déclarateur d'un membre de la structure ou de l'union. Cependant, un membre de structure peut aussi être constitué d'un nombre de bits donnés. Un tel membre s'appelle un champ de bits, ou un champ tout court ; sa longueur est séparée du déclarateur donnant son nom par un signe deux-points.

    déclarareur-de-struct :

      déclarareur
      déclarareur-      opt : expression-constante

Un spécificateur de type de la forme

    struct-ou-union identificateur-opt {liste-de-déclarations-de-struct }

déclare que l'identificateur est l'étiquette de la structure ou de l'union indiquée par la liste. Les déclarations ultérieures figurant dans les limites de la portée courante peuvent faire référence au même type en spécifiant cette étiquette, sans la liste associée :

    struct-ou-union identificateur

Si un spécificateur ne comportant pas de liste figure avant que son étiquette ne soit définie, il en résulte un type incomplet. On peut se servir des objets de type structure ou union incomplet aux endroits où l'on a pas besoin de connaître leur taille, par exemple dans des déclarations (pas des définitions), pour spécifier un pointeur, ou pour créer un typedef, à l'exclusion de tous autres cas, Le type en question devient complet dès l'apparition d'un spécificateur portant la même étiquette et contenant une liste de déclarations. Même dans le cas des spécificateurs comportant une liste, le type structure ou union en cours de déclaration est incomplet à l'intérieur de la liste ; il ne devient complet qu'à partir du signe } qui termine le spécificateur.

Aucun membre d'une structure ne peut être de type incomplet. Par conséquent, il est impossible de déclarer une structure ou une union qui contienne une instance d'elle-même. Toutefois, en plus de baptiser le type structure ou union concemé, les étiquette permettent de définir des structures auto-référentes ; une structure ou une union peut en effet contenir un pointeur sur une instance d'elle-même, car on peut déclarer des pointeurs sur des types incomplets.

Une règle très particulière s'applique aux déclarations de la forme

    struct-ou-union identificateur ;

qui déclarent une structure ou une union, mais ne comportent ni liste de déclarations, ni déclarateurs. Même si l'identificateur est une étiquette de structure ou d'union déjà déclarée et visible (§11.1), une telle déclaration fait de l'identificateur l'étiquette d'une nouvelle structure ou union, de type incomplet, pour la portée courante.

      Cette règle obscure est une nouveauté de la norme ANSI. Elle sert à limiter les structures mutuellement récursives déclarées dans la portée courante, mais dont les étiquettes peuvent déjà avoir été déclarées dans une portée de niveau supérieur.

Un spécificateur de structure ou d'union comportant une liste, mais pas d'étiquette, crée un type unique, dont on ne peut se servir directement que dans la déclaration dont il fait partie.

Les noms de membres et d'étiquettes ne peuvent pas entrer en conflit entre eux, ni avec ceux des variables ordinaires. Un même nom de membre ne peut pas figurer deux fois dans la même structure ou union, mais il peut servir dans des structure ou des unions différentes.

      Dans la première édition de ce livre, les noms des membre de structure et d'union n'étaient pas associés à leur parent. Cependant les compilateurs réalisaient couramment cette association bien avant la norme ANSI.

Les membre de structure ou d'union, à l'exception des champ de bits, peuvent être de n'importe quel type. Les champs de bits (qui n'ont pas obligatoirement de déclarateur, et peuvent donc ne pas porter de nom) sont de type int, unsigned int ou signed int, et sont interprétés comme des objets de type entier dont la longueur en bits est donnée ; le fait que les champs de type int soient considérés comme signés ou non dépend de l'implémentation. Les champs adjacents dans une structure sont regroupés dans des unités de mémoire dépendant de l'implémentation dans un sens dépendant de l'implémentation. Lorsqu'un champ suivant un autre champ ne rentre pas dans une unité de mémoire partiellement remplie, il peut être divisé entre deux unités, ou l'unité en question peut être complétée à vide. Un champ nom nommé de largeur 0 force ce remplissage complémentaire, afin que le champ suivant commence au début de l'unité d'allocation suivante.

      La norme ANSI rend les champs encore plus dépendants de l'implémentation que ne le faisait la première édition. Il est conseillé de considérer que les régles du langage concernant la mémorisation des champs de bits dépendent entièrement de l'imp1émentation. On peur employer des structures comportant des champs de bits dans le but de réduire la taille mémoirc nécessaire à ces structures, de façon portable (et probablernent au prix d'une augmentation du nombre d'instructions et du temps d'exécution nécessaires pour accéder à ces champs), ou bien pour décrire une structure de données au niveau du bit de façon non portable. Dans ce cas, il est impératif de connaître les règles de l'implémentation utilisée.

Les membres d'une structure se situent à des adresses croissantes dans l'ordre de leurs déclarations, Les membres autres que des champs sont alignés sur une limite d'adresse qui dépend de leur type ; par conséquent, une structure peut contenir des vides non baptisés. Si l'on convertit le type d'un pointeur sur une structure en celui d'un pointeur sur son premier membre, le résultat pointe sur ce premier membre.

On peut considérer une union comme une structure dont tous les membres commencent à son début (déplacement 0), et de taille suffisante pour contenir n'importe lequel d'entre eux. Une union ne peut contenir qu'un seul de ses membres à un instant donné, Si l'on convertit le type d'un pointeur sur une union en celui d'un pointeur sur l'un de ses membres, le résultat pointe sur ce membre.

Voici un exemple simple de déclaration de structure :

    struct noeud {
        char mot [20];
        int nb;
        struct noeud *gauche;
        struct noeud *droite;
};

qui contient un tableau de 20 caractères, un entier et deux pointeurs sur des structures semblables. Après la déclaration de cette structure, la déclaration

    struct noeud s, *ps;

déclare s comme une structure de cette forme et ps comme un pointeur sur une telle structure. Avec ces déclarations, l'expression

    ps->nb

désigne le champ nb de la structure pointée par ps;

    s.gauche

désigne le pointeur sur le sous-arbre gauche de la structure s ; et

    s.droite->mot[0]

désigne le premier caractère du membre mot du sous-arbre droit de s.

En général, on ne peut pas lire un membre d'une union sans avoir affecté auparavant une valeur à cette union via ce même membre. Toutefois, il existe une règle particuliére qui simplifie l'usage des unions : si une union contient plusieurs structures ayant une partie initiale commune, et si cette union a pour valeur actuelle l'une de ces structures, on peut faire référence à la partie initiale commune de n'importe laquelle des structures en question. Par exemple, ceci est un fragment de programme valide :

      union {
    struct {
        int type;
    } n;
    struct {
        int type;
        int noeud int;
    } ni;
    struct {
        int type;
        float noeud float;
    } nf;
} u;
...
u.nf.type = FLOAT;
u.nf.noeud_float = 3.14;
...
if (u.n.type == FLOAT)
... sin(u.nf.noeud_float) ...

8.4 Les énumérations

Les énumérations sont des types uniques dont les valeurs possibles font partie d'un ensemble de constantes ponant un nom, que l'on appelle des énumérateurs. La forme d'un spécificateur d'énumération est similaire à celle des structures et des unions.

    spécificateur-d'énumération :

      enum identificateur-opt { liste-d'énumérateurs }
      enum identificateur

    liste-d'énumérateurs :

      énumérateur
      liste-d'énumérateurs , énumérateur

    énumérateur:

      identificateur = expression-constante

Les identificateurs faisant partie d'une liste d'énumérateurs sont déclarés comme des constantes de type int, et peuvent figurer partout où des constantes sont nécessaires. Si aucun des énumérateurs ne comporte un signe =, la première constante correspondante vaut 0 et les suivantes augmentent de 1 en 1 de gauche à droite. Un énumérateur componant un signe = donne à l'identificateur concerné la valeur indiquée ; les identificateurs suivants continuent la progression de 1 en 1 à partir de cette valeur.

Les noms d'énumérateurs de même portée doivent être tous différents, et distincts des noms de variables ordinaires ; cependant, leurs valeurs ne sont pas toutes obligatoirement distinctes.

L'identificateur d'un spécificateur d'énumération joue le même rôle que celui de l'étiquette d'une structure ; il donne un nom à une énumération particulière. Les règles concernant les spécificateurs d'énumérations comportant ou non une étiquette ou une liste sont les mêmes que dans le cas des spécificateurs de structures ou d'unions, mis à part qu'ù n'existe pas de types incomplets d'énumérations ; l'étiquette d'un spécificateur d'énumération ne comportant pas de liste d'énumérateurs doit se rapporter à un spécificateur visible comportant une liste.

      Les énumétations n'existaient pas dans la premiére édition de ce 1ivre, mais elles font partie du langage depuis quelques années.

8.5 Les déclarateurs

La syntaxe des déclarateus est la suivante :

    déclarateur :

      pointeur-opt déclarateur-absolu

    déclarateur-absolu :

      identificateur
      ( déclarateur )
      déclarateur-absolu [ expression-constante-opt ]
      déclarateur-absolu ( liste-de-types-de-paramètres )
      déclarateur-absolu ( liste-d'identificateurs-opt )

    pointeur :

      * liste-de-qualificatifs-de-type-opt
      * liste-de-qualificatifs-de-type-opt pointeur

    liste-de-qualificatifs-de-type:

      qualificatif-de-type
      liste-de-qualificatifs-de-type qualificatif-de-type

La structure des déclarateurs ressemble à celle des expressions d'indirection, de fonctions et de tableaux ; ils se groupent de la même manière.

8.6 La signification des déclarateurs

Une liste de déclarateurs figure après une séquence de spécificateurs de type et de classe de stockage. Chaque déclarateur déclare un identificateur principal unique, celui qui figure comme première possibilité de production pour le déctarateur-absolu. Le spécificateur de classe de stockage s'applique directement à cet identificateur, mais son type dépend de la forme de son déclarateur. Un déclarateur se lit comme une assertion énonçant que lorsque son identificateur figure dans une expression de même forme que le déclarateur, il donne un objet du type indiqué.

Si l'on se borne à examiner les parties des spécificateurs de déclaration concernant le type (§8.2) et un déclarateur particulier, une déclaration est de la forme "T D", où T est un type et D un déclarateur. Cette notation décrit de façon inductive le type attribué à l'identificateur selon la forme du déclarateur.

Dans une déclaration T D où D est un simple identificateur, cet identificateur est de type T.

Dans une déclaration T D où D est de la forme

    ( D1 )

l'identificateur figurant dans D1 est de même type que celui de D. Les parenthèses ne modifient pas le type, mais elles peuvent changer l'interprétation des déclarateurs complexes.

8.6.1 Les déclarateurs de pointeurs

Dans une déclaration T D où D est de la forme

    * liste-de-qualifitcatifs-de-type-opt D1

et où l'identificateur figurant dans la déclaration T D1 est de type "modificateur-de-type T", l'identificateur de D est de type "modificateur-de-type liste-de-qualificatifs-de-type pointeur sur T". Les qualificatifs qui suivent le signe * s'appliquent au poin-teur même, et non à l'objet sur lequel il pointe. '

Examinons par exemple la déclaration

    int *tp[];

Ici, c'est tp[] qui joue le rôle de D1 ; une déclaration " int tp[] " (plus loin) donnerait à tp le type "tableau de int ", la liste de qualificatifs de type est vide et le modificateur de type est "tableau de". Par conséquent, cette déclaration donne à tp le type "tableau de pointeurs sur int ".

Pour prendre d'autres exemples, les déclarations

    int i, *pi, *const cpi = &i;
    const int ci = 3, *pci;

déclarent un entier i et un pointeur sur un entier pi. On ne peut pas modifier la valeur du pointeur constant cpi ; il pointera toujours sur le même emplacement, mais on pourra néanmoins modifier la valeur à laquelle il fait référence. L'entier ci est constant, on ne peut donc pas le modifier (bien que l'on puisse l'initialiser, comme ici). pci est de type "pointeur sur const int ", et l'on peut modifier pci lui-même afin de le faire pointer ailleurs, mais on ne peut pas modifier la valeur sur laquelle il pointe par une affectation via pci.

8.6.2 Les déclarateurs de tableaux

Dans une déclaration T D où D est de la forme

    D1[expression-constante-opt]

et où l'identificateur figurant dans la déclaration T D1 est de type "modificateur-de-type T", l'identificateur de D est de type "modificateur-de-type tableau de T". Si une expression constante est présente, elle doit être de type entier, et de valeur supérieure à 0. Si cette expression constante est absente, le tableau est de type incomplet.

On peut construire un tableau à partir d'un type arithmétique, d'un pointeur, d'une structure ou d'une union, ou d'un autre tableau (ce qui donne un tableau à plusieurs dimensions). On ne peut construire un tableau qu'à partir de types complets ; on ne peut pas construire un tableau de structures de type incomplet. Ainsi, dans le cas d'un tableau à plusieurs dimensions, seule la première dimension peut être absente. Le type d'un objet de type tableau incomplet se complète par une autre déclaration, complète, de cet objet (§10.2), ou par une initialisation (§8.7). Par exemple,

    float tf [17] , *tpf [17] ;

déclare un tableau de nombres flottants et un tableau de pointeurs sur des nombres flottants. Et

    static int x3d[3][5][7];

déclare un tableau d'entiers statique à trois dimensions, de taille 3x5x7. Plus exactement, x3d est un tableau de trois éléments, dont chacun est un tableau de cinq tableaux ; chacun de ces derniers tableaux contient sept entiers. Les expressions x3d, x3d[i] , x3d[i][j] et x3d[i][j][k] sont toutes valides. Les trois premières sont de type "tableau", la demière de type int. Plus précisément, x3d[i][j] est un tableau de 7 entiers, et x3d[i] est un tableau de 5 tableaux de 7 entiers.

L'opération d'indexation des tableaux est définie de façon, que E1[E2] soit identique à *(E1+E2). Par conséquent, malgré son aspect asymétrique, l'indexation est une opération commutative. A cause des règles de conversion qui s'appliquent à l'opérateur + et aux tableaux (§§6.6, 7.1, 7.7), si E1 est un tableau et E2 un entier, E1[E2] désigne l'élément de E1 d'indice E2.

Dans l'exemple ci-dessus, x3d[i][j][k] équivaut à *(x3d[i][j]+k) . La première sous-expression x3d[i][j] est convertie dans le type "pointeur sur un tableau d'entiers", d'après le §7.1 ; et d'après le §7.7, l'addition s'effectue après avoir multiplié k par la taille d'un entier. Ces règles impliquent que les tableaux sont, mémorisés par rangées (c'est le demier indice qui varie le plus vite), et que le premier indice figurant dans la déclaration permet de déterminer la place mémoire occupée par un tableau, mais ne joue aucun autre rôle dans les calculs d'indices.

8.6.3 Les déclarateurs de fonctions

Dans une déclaration de fonction sous la nouvelle forme T D où D est de la forme

    D1 ( liste-de-types-de-paramètres )

et où l'identificateur figurant dans la déclaration T D1 est de type "modificateur-de-type T", l'identificateur de D est de type "modificateur-de-type fonction d'arguments liste-de-types-de-paramètres retournant T".

La syntaxe des paramètres est la suivante :

    liste-de-types-de-paramètres :

      liste-de-paramètres
      liste-de-paramètres       , ...

    liste-de-paramètres :

      déclaration-de-paramètres
      liste-de-paramètres       , déclaration-de-paramètre

    déclaration-de-paramètres:

      spécificateurs-de-déclaration déclarateur
      spécificateur-de-déclaration déclarateur-abstrait      -opt

Dans la nouvelle forme des déclarations, la liste de paramètres indique les types des paramètres. Dans le cas particulier d'un déclarateur de fonction sous la nouvelle forme ne comportant pas de paramètres, la liste de types de paramètres est constituée du seul mot-clé void. Si la liste de types de paramètres se termine par une ellipse ",...", la fonction peut accepter plus d'arguments que le nombre de paramètres décrits explicitement (cf. §7.3.2).

Les paramètres de type tableau ou fonction sont transformés en pointeurs, en accord avec les règles de conversion des paramètres (cf. §10,1). Le seul spécificateur de classe de stockage autorisé dans un spécificateur de déclaration de paramètres est register, et il n'est pris en compte que si le déclarateur de fonction est suivi d'une définition de fonction. De même, si les déclarateurs figurant dans les déclarations de paramètres comportent des identificateurs et si le déclarateur de fonction n'est pas suivi d'une définition de fonction, ces identificateurs deviennent immédiatement invisibles. Les déclarateurs abstraits, qui ne précisent pas les identificateurs, sont présentés au §8.8.

Dans une déclaration de fonction sous l'ancienne forme T D où D est de la forme

    D1 ( liste-d'identificateurs-opt )

et où l'identificateur figurant dans la déclaration T D1 est de type "modificateur-de-type T", l'identificateur de D est de type "modificateur-de-type fonction d'arguments non spécifiés retournant T". Les paramètres éventuels sont de la forme

    liste-d'identificateurs :

      identificateur liste-d'identificateurs , identificateur

Dans un déclarateur sous l'ancienne forme, la liste d'identificateurs ne doit figurer que si ce déclarateur est placé en tête d'une définition de fonction (§10.1). La déclaration ne fournit aucun renseignement sur les types des paramètres.

Par exemple, la déclarationint

    f(), *fpi(), (*pfi)();

déclare une fonction f retournant un entier, une fonction fpi retournant un pointeur sur un entier, et un pointeur pfi pointant sur une fonction retournant un entier. Aucune de ces déclarations ne comporte les types des paramètres ; elles sont sous l'ancienne forme.

D'après la déclaration sous la nouvelle forme

    int strcpy (char *dest, const char *source), rand(void) ;

strcpy est une fonction retournant un int qui accepte deux arguments, le premier étant un pointeur de caractère et le second un pointeur sur une chaîne de caractères constante. Les noms des paramètres servent en fait de commentaires. La deuxième fonction, rand, ne prend pas d'arguments et retourne un int.

      Les déclarateurs de fonction comportant des prototypes de paramètres constituent de loin la plus grande modification apportée au langage par la norme ANSI. Leur avantage par rapport aux "anciens" déclarateurs de la première édition est qu'ils permettent de détecter des erreurs et de forcer les types des arguments pour les appels de fonctions, mais c'est au prix de bouleversements et de confusions lors de leur introduction, et de la nécessité de pouvoir traiter les deux formes. Pour assurer cette compatibilité, certaines syntaxes sont peu heureuses, en particulier le void servant à marquer explicitement les fonctions sous la nouvelle forme ne comportant pas de paramètres.
      La notation d'ellipse ",..." désignant les fonctions à liste variable d'arguments est également une nouveauté, et elle formalise, avec les macros définies dans l'en-t6te standard <stdarg.h>, un mécanisme qui était officiellement interdit mais officieusement toléré dans la première édition.
      Ces notations sont inspirées du langage C++.

8.7 L'initialisation

Au moment de la déclaration d'un objet, son déclarateur-init peut spécifier une valeur initiale pour l'identificateur déclaré. L'initialisateur est précédé de =, et il est constitué soit d'une expression, soit d'une liste d'initialisateurs imbriqués entre accolades. Une telle liste peut se terminer par une virgule, pour de simples questions d'esthétique.

    initialisateur :

      expression-d'affectation
      { liste-d'initialisateurs }
      {       liste-d'initialisateurs , }

    liste-d'initialisateurs :

      initialisateur
      liste-d'initialisateurs       , initialisateur

Toutes les expressions figurant dans l'initialisateur d'un objet ou d'un tableau statique doivent être des expressions constantes, décrites au §7,19. Les expressions figurant dans l'initialisateur d'un objet ou d'un tableau de classe auto ou register doivent aussi être des expressions constantes si cet initialisateur est une liste entre accolades. Toutefois, si l'initialisateur d'un objet automatique est constitué d'une seule expression, celle-ci n'est pas obligatoirement une expression constante, elle doit seulement être de type convenable pour l'affectation à cet objet.

      La première édition n'admettait pas que l'on initialise des structures, des unions ou des tableaux automatiques. La norme ANSI autorise ces initialisations, mais uniquement avec des constructions constantes, sauf si l'on peut exprimer l'initialisateur grâce à une seule expression.

Un objet statique qui n'est pas initialisé explicitement prend pour valeur initiale la constante 0. Dans le cas d'un objet statique composé, tous ses éléments sont initialisés à 0. La valeur initiale d'un objet automatique non initialisé n'est pas définie.

Initialisateur d'un pointeur ou d'un objet de type arithmétique est constitué d'une seule expression, éventuellement entre accolades. Cette expression est affectée à l'objet en question.

Initialisateur d'une structure est soit une expression du même type, soit une liste initialisateur placée entre accolades, correspondant à chacun de ses membres, dans l'ordre. Si cette liste comporte moins d'initialisateur que le nombre de membres de la structure, les derniers membres prennent la valeur initiale 0. Il est interdit de donner plus d'initialisateurs qu'il n'y a de membres.

Initialisateur d'un tableau est une liste d'initialisateurs placée entre accolades, correspondant à chacun de ses membres. Si la taille du tableau n'est pas précisée, elle est déterminée par le nombre d'initialisateurs, et le type du tableau devient complet. Si la taille du tableau est fixée, le nombre d'initialisateurs ne doit pas dépasser le nombre d'éléments du tableau ; si l'on en donne moins, les derniers éléments prennent la valeur initiale 0.

Il existe un cas particulier : on peut initialiser un tableau de caractères par une constante de type chaîne ; les caractères successifs de la chaîne initialisent un à un les éléments du tableau. De même, une constante de type chaîne de caractères étendus (§2.6) peut initialiser un tableau de type wchar_t. Si la taille du tableau n'est pas précisée, elle est déterminée par le nombre de caractères de la chaîne, y compris le \0 final ; si la taille du tableau est fixée, le nombre de caractères de la chaîne, non compris le \0 final, ne doit pas dépasser la taille du tableau.

Initialisateur d'une union peut être constitué soit d'une expression unique de même type, soit d'un initialisateur du type du premier membre de l'union, placé entre accolades.

      La première édition interdisait l'initialisation des unions. La règle du "premier membre" est maladroite, mais il est difficile de la généraliser sans introduire une nouvelle syntaxe. A côté du fait qu'elle autorise l'initialisation explicite des unions, du moins sorts une forme primitive, cette règle de l'ANSI définit la sémantique des unions statiques non initialisées.

Un agrégat est une structure ou un tableau. Si un agrégat comporte des éléments de type agrégat, les règles d'initialisation s'appliquent récursivement. On peut omettre des accolades dans l'initialisation d'après les règles suivantes : si l'initialisateur d'un élément d'agrégat étant lui-même un agrégat commence par une accolade ouvrante, alors la liste suivante d'initialisateurs séparés par des virgules initialise les éléments du sous-agrégat ; cette liste ne doit pas comporter plus d'initialisateurs que le nombre d'éléments du sous-agrégat. Cependant, si l'initialisateur d'un sous-agrégat ne commence pas par une accolade ouvrante, alors on utilise juste assez d'éléments de la liste pour initialiser le sous-agrégat ; les éléments restants éventuels servent à initialiser l'élément suivant de l'agrégat dont fait partie le sous-agrégat.

Par exemple,

    int x[] = ( 1, 3, 5 );

déclare et initialise x comme un tableau à une dimension comportant trois éléments, car sa taille n'est pas précise et il y a trois initialisateurs.

    float y[4][3] = {
    { 1, 3, 5 ),
    { 2, 4, 6 ),
    { 3, 5, 7 ),
};

est une initialisation où figurent toutes les accolades : 1, 3 et 5 initialisent la première rangée du tableau y[0] , c'est-à-dire y[0][0] , y[0][1] et y[0][2]. De même, les deux lignes suivantes initialisent y[1] et y[2]. L'initialisateur s'arrête à ce moment, si bien que les éléments de y[3] sont initialisés à 0. On aurait obtenu exactement le même résultat en écrivant

    f1oat y[4][3] = {
    1, 3, 5, 2, 4, 6, 3, 5, 7
};

L'initialisateur de y commence par une accolade ouvrante, mais pas celui de y[0] ; par conséquent, y[0] est initialisé par les trois premiers éléments de la liste. De même, les trois suivants servent à initialiser y[1] , et les trois derniers y[2] . Quant à la ligne

    float y[4][3] = {
    { 1 } , { 2 }, { 3 }, { 4 }
};

elle initialise la première colonne de y (considéré comme un tableau à deux dimensions) et met le reste à 0.

Enfin,

    char msg[] = "Erreur de syntaxe en ligne %s\n";

est un exemple de tableau de caractères dont les éléments sont initialisés par une chaîne ; sa taille est calculée en incluant le caractère \0 final.

8.8 Les noms de types

Dans divers contextes (pour réaliser des conversions de types explicites par des "casts", pour déclarer les types des paramètres dans les déclarateurs de fonction, et pour l'opérande de sizeof), il faut indiquer le nom d'un type de données. Cela se réalise grâce à un nom de type, dont la syntaxe est celle d'une déclaration d'objet de ce type où l'on omet le nom de l'objet.

    nom-de-type :

      liste-de-qualificatifs-de-spécifcateurs déclarateur-abstrait-opt

    déclarateur-abstrait:

      pointeur pointeur-opt
      déclarateur-abstrait-absolu

    déclarateur-abstrait-absolu :

      ( déclarateur-abstrait )
      déclarateur-abstrait-absolu-opt [ expression-constante-opt ]
      déclarateur-abstrait-absolu-opt ( liste-de-types-de-paramètres-opt )

On peut se contenter d'indiquer l'endroit du déclarateur abstrait où figurerait l'identificateur si cette construction était le déclarateur d'une déclaration. Le nom de type ainsi obtenu représente le type de cet identificateur hypothétique. Par exemple,

    int
    int *
    int *[3]
    int (*)[]
    int *()
    int (*[])(void)

sont respectivement les noms des types "entier", "pointeur sur un entier", "tableau de 3 pointeurs sur des entiers", "pointeur sur un tableau d'entiers de taille indéfinie", "fonction de paramètres indéfinis retournant un pointeur sur un entier" et "tableau, de taille indéfinie, de pointeur sur des fonctions sans paramètres, retournant chacune un entier".

8.9 Typedef

Les déclarations dont le spécificateur de classe de stockage est typedef ne déclarent pas des objets, mais définissent des identificateurs représentant des types. Ces identificateurs sont appelés des noms typedef.

    nom-typedef :

      identificateur

Une déclaration de classe typedef attribue un type à chacun des noms de ses déclarateurs, de la façon habituelle (cf. §8.6). Par la suite, chacun de ces noms typedef équivaut syntaxiquement à un spécificateur du type qui lui est associé.

Par exemple, après

    typedef long Numbloc, *Ptrbloc;
    typedef struct ( double r, theta; ) Complexe;

les constructions

    Numbloc b;
    extern Ptrbloc pb;
    Complexe z, *pz;

sont des déclarations correctes. b est de type long, pb de type "pointeur sur long", le type de z est la structure indiquée et pz est un pointeur sur une telle structure.

typedef n'introduit pas de nouveaux types, mais des synonymes pour des types que l'on pourrait spécifier autrement. Dans notre exemple, b est de même type que tout autre objet de type long.

On peut redéclarer des noms typedef dans une portée plus restreinte, mais il faut alors donner un ensemble non vide de spécificateurs de type. Par exemple,

    extern Numbloc;

ne redéclare pas Numbloc, alors que

    extern int Numbloc;

le redéclare.

8.10 Les équivalences de types

Deux listes de spécificateurs de type sont équivalentes si elles contiennent les mêmes spécificateurs, en tenant compte du fait que certains spécificateurs en impliquent d'autres (par exemple, long tout court signifie long int). Les structures, les unions et les énumérations dont les étiquettes différent sont distinctes, et une structure, une union ou une énumération sans étiquette spécifie un type unique.

Deux types sont identiques si leurs déclarateurs abstraits (§8.8), après développement des types typedef éventuels et suppression des identificateurs des paramètres de fonctions, sont les mêmes, y compris leurs listes de spécificateurs de type. Les tailles des tableaux et les types des paramètres de fonctions sont significatifs.

9. Les instructions

A l'exception de certains cas particuliers, les instructions sont exécutées les unes à la suite des autres. Les instructions sont exécutées pour les effets qu'elles produisent et n'ont pas de valeurs. On les classe en plusieurs groupes.

    instruction :

      instruction-étiquetée
      instruction-expression
      instruction-composée
      instruction-de-sélection
      instruction-d'itération
      instruction-de-saut

9.1 Les instructions étiquetées

Les instructions peuvent être précédées d'une étiquette.

    instruction-étiquetée :

      identigicateur : instruction
      case expression-constante : instruction
      default : instruction

Une étiquette constituée d'un identificateur déclare cet identificateur. Le seul usage possible d'une étiquette composée d'un identificateur est de servir de référence à un goto. La portée de l'identificateur est limitée à la fonction courante. Puisque les étiquettes possèdent leur propre classe de noms, elles ne peuvent interférer avec les autres identificateurs et ne peuvent pas être redéclarées. Voir §1 1.1.

On utilise les étiquettes de type case et default avec l'instruction switch (§ A9.4). L'expression constante employée avec case doit être de type entier.

Les étiquettes ne modifient pas en elles-mêmes le déroulement d'un programme.

9.2 Les instructions-expressions

La plupart des instructions sont des instructions-expressions de la forme

    instruction-expression :

      expression-opt ;

La plupart d'entre elles sont des affectations ou des appels de fonctions. Tous les effets de bord de l'expression sont pris en compte avant que l'instruction suivante ne soit exécutée. S'il n'y pas d'expression, on appelle cette construction une instruction nulle ; on utilise souvent une telle construction pour fournir un corps vide à une instruction de bouclage ou pour placer une étiquette.

9.3 Les instructions composées

Pour pouvoir mettre plusieurs instructions là où une seule est attendue, on peut utiliser une instruction composée (appelée également "bloc"). Le corps de la définition d'une fonction est une instruction composée.

    instruction-composée :

      { liste-de-déclarations-opt liste-d'instructions-opt )

    liste-de-déclarations:

      déclaration
      liste-de-déclarations déclaration

    liste-d'instructions :

      instruction
      liste-d'instructions instruction

Si un identificateur de la liste des déclarations est déjà visible à l'extérieur du bloc, la déclaration extérieure est suspendue à l'intérieur du bloc (voir §11.1), après lequel elle sera de nouveau visible. On ne peut déclarer qu'une seule fois un identificateur dans le même bloc. Les règles sappliquent aux identificateurs d'une même classe de noms (§ 11); on traite de façon distincte les identificateurs des différentes classes de noms.

L'initialisation des objets dynamiques est effectuée à chaque fois qu'on entre dans le bloc par sa partie supérieure et dans l'ordre des déclarations. Si l'on exécute un saut à l'intérieur du bloc, on n'effectue pas ces initialisations. Les initialisations des objets static ne sont réalisées qu'une seule fois avant que le programme ne commence son exécution.

9.4 Les instructions de sélection

Les instructions de sélection permettent d'effectuer des choix entre différentes parties d'un programme.

    instruction-de-sélection :

      if ( expression ) instruction
      if ( expression ) instruction e1se instruction
      switch ( expression ) instruction

Dans les deux formes de l'instruction if, l'expression qui doit être de type arithmétique ou pointeur, est évaluée, y compris les effets de bord, et si le résultat est différent de 0, on exécute la première sous-instruction. Dans la seconde forme, on exécute la seconde sous-instruction si l'expression est égale à 0. L'ambiguïté de e1se est résolue en reliant un else à la dernière instruction if n'ayant pas de else correspondant et se trouvant au même niveau d'imbrication de bloc.

L'instruction switch permet de diriger le programme vers une instruction choisie parmi plusieurs, suivant la valeur d'une expression qui doit être de type entier. La sous-instruction contrôlée par un switch est généralement composée. Chaque instruction à l'intérieur de la sous-instruction peut être précédée d'une ou plusieurs étiquettes case (§9.1). L'expression de contrôle est transformée en valeur entière, et les constantes employées pour les différents cas le sont également. Il ne faut pas que deux constantes correspondant à deux cas différents et associées au même switch aient la même valeur après conversion. Egalement, il ne peut y avoir au plus qu'une seule étiquette default dans un switch. On peut imbriquer les instructions switch ; une étiquette case ou default est associée avec le plus petit switch qui la contient.

Quand l'instruction switch est exécutée, son expression est évaluée y compris tous les effets de bord et comparée avec les constantes associées à chaque cas. Si l'une des constantes est égal à la valeur de l'expression, le programme exécute l'instruction de l'étiquette case correspondante. Si aucune des constantes n'est égale à l'expression et s'il y a une étiquette default le programme exécute l'instruction correspondante. Si aucune constante ne convient et s'il n'y a pas d'étiquette default, alors aucune sous-instruction de l'aiguillage n'est exécutée.

      Dans la première édition de ce livre, l'expression de contrôle de switch et les constantes associées aux différents cas devaient être de type int.

9.5 Les instructions d'itération

Les instructions d'itération spécifient le rebouclage.

    instruction-d'itération :

      while ( expression ) instruction
      do instruction while ( expression ) ;
      for ( expression-opt ; expression-opt ; expression-opt ) instruction

Dans les instructions while et for, la sous-instruction est exécutée de manière répétitive tant que la valeur de l'expression est différente de 0 ; l'expression doit être de type arithmétique ou de type pointeur. En ce qui concerne while, le test avec tous les effets de bord engendrés par l'expression est effectué avant chaque exécution de l'instruction ; pour do, le test est réalisé à la fin de l'itération.

Dans l'instruction for, la première expression n'est évaluée qu'une fois, et spécifie donc l'initialisation de la boucle. Il n'y a aucune restriction sur son type. La seconde expression doit être de type arithmétique ou de type pointeur ; elle est évaluée avant chaque itération et, si elle devient égale à 0, le for se termine. La troisième expression est évaluée après chaque itération et spécifie donc une ré-initialisation de la boucle. Il n'y a aucune restriction sur son type. Les effets de bord de chaque expression sont immédiatement pris en compte après leur évaluation. Si la sous-instruction ne contient pas continue, l'instruction ,

    for ( expression1 ; expression2 ; expression3 ) instruction

est équivalente à

    expression1 ;
while ( expression2 ) {
        instruction
expression3 ;
}

On peut omettre n'importe laquelle des trois expressions. Si la seconde expression est absente, le test implicite équivaut à tester une constante non nulle.

9.6 Les instructions de saut

Les instructions de saut modifient le déroulement du programme sans conditions.

    instruction-de-saut :

      goto identificateur ;
      continue ;
      break ;
      return       expression-opt ;

Dans l'instruction goto, l'identificateur doit être une étiquette (§9,1) située dans la fonction courante. L'exécution continue à l'instruction qui suit cette étiquette. On ne peut trouver une instruction continue qu'à l'intérieur d'une instruction de bouclage, Cette instruction dirige le programme vers la fin de la plus petite boucle qui l'entoure, Plus précisément, dans chacune des instructions ci-dessous :

    while ( ... ) {
    ...
suite: ;
}
    do {
    ...
suite: ;
} while ( ... );
    for ( ... ) {
    ...
suite: ;
}

un continue qui ne fait pas partie d'une instruction d'itération imbriquée équivaut à un goto suite.

On ne peut trouver l'instruction break que dans une instruction de bouclage ou une instruction switch ; break termine l'exécution de la plus petite des instructions qui l'entourent. L'exécution reprend à l'instruction qui suit l'instruction terminée.

Une fonction retourne au programme appelant à l'aide de l'instruction return. Quand return est suivie d'une expression, la valeur de celle-ci est retournée au programme appelant. L'expression est convertie, de la même façon que pour une affectation, dans le type retourné par la fonction dans laquelle elle se trouve.

Omettre la fin d'une fonction est équivalent à un retour sans expression. Dans un tel cas, la valeur de retour est indéterminée.

10. Les déclarations externes

L'unité d'entrée fournie au compilateur C s'appelle une unité de traduction ; elle consiste en une suite de déclarations externes qui sont soit des déclarations, soit des définitions de fonctions.

    unité-de-traduction :

      déclaration-externe
      unité-de-traduction déclaration-externe

    déclaration-externe :

      définition-de-fonction
      déclaration

La portée des déclarations externes s'étend jusqu'à la fin de l'unité de traduction dans laquelle elles sont déclarées, exactement de la même façon que l'effet des déclarations internes d'un bloc s'étend jusqu'à la fin du bloc. La syntaxe des déclarations externes est la même que celle des autres déclarations, mis à part qu'on ne peut donner le code des fonctions qu'à ce niveau.

10.1 Les définitions de fonctions

Les définitions de fonctions ont la forme suivante

    définition-de-fonction :

      spécificateur-de-déclaration-opt déclarateur liste-de-déclarations-opt instruction-composée

Les seuls spécificateurs de classe de stockage autorisés sont extern et static ; voir §11.2 pour les différencier.

Une fonction peut retourner un type arithmétique, une structure, une union, un pointeur ou void, mais pas une fonction ou un tableau. Le déclarateur d'une déclaration de fonction doit indiquer explicitement que le spécificateur déclaré est de type fonction ; c'est-à-dire qu'il doit contenir l'une des formes suivantes (voir § 8.6.3)

    déclarateur-absolu ( liste-de-paramétres-avec-types )
    déclarateur-absolu     ( liste-d'identtificateurs-opt )

où le déclarateur-absolu est un identificateur ou un identificateur parenthésé. En particulier, il ne doit pas acquérir le type fonction à l'aide d'un typedef.

Sous la première forme, la définition est une fonction d'un nouveau style et ses paramètres, accompagnés de leur type, sont déclarés dans la liste de paramètres avec types ; la liste de déclarations qui suit le déclarateur doit être absente. A moins que la liste de paramètres avec types ne contienne que void indiquant que la fonction n'a pas de paramètres, chaque déclarateur dans la liste de paramètres avec types doit contenir un identificateur. Si la liste de paramètres avec types se termine par ",...", alors la fonction peut être appelée avec plus d'arguments que de paramètres ; pour faire référence aux arguments supplémentaires, on doit utiliser le mécanisme de la macro va_arg défini dans le fichier d'en-tête <stdarg.h> et décrit à l'annexe B. Les fonctions utilisant une telle liste variable de paramètres doit avoir au moins un paramètre nommé.

Dans la seconde forme, la définition suit l'ancien style : la liste d'identificateurs donnent le nom des paramètres, pendant que la liste des déclarations donnent leur type. Si aucune déclaration n'est donnée pour un paramètre, son type est supposé être int. La liste de déclarations doit déclarer uniquement les paramètres indiqués dans la liste, il est impossible de les initialiser et register est le seul spécificateur de classe de stockage possible.

Dans les deux styles de définition de fonction, les paramètres sont supposés avoir été déclarés juste après le début de l'instruction composée qui forme le corps de la fonction et, par conséquent, les mêmes identificateurs ne doivent pas être redéclarés ici (bien qu'ils pourraient être, comme les autres identificateurs, redéclarés dans des blocs internes). si on déclare un paramètre de type "tableau de type", la déclaration est transformée en "pointeur de type" ; de même, si on déclare un paramètre de type "fonction retournant un type", la déclaration est transformée en "pointeur de fonction retournant un type". A l'appel d'une fonction, les arguments sont convertis si nécessaire et affectés aux paramètres ; voir §7.3.2.

Les définitions de fonctions sous la nouvelle forme sont une nouveauté de la norme ANSI. Il y a également un léger changement dans les détails de la promotion ; la première édition indiquait que les déclarations de paramètres float étaient transformés en double. La différence devient digne d'intérêt lorsqu'un pointeur sur un paramètre est généré à l'intérieur d'une fonction.

Voici un exemple complet de définition de fonction sous la nouvelle formeint

    max(int a, int b, int c)
{
    int m;
    m = (a > b) ? a : b;
    return (m > c) ? m : c;
}

Ici, la déclaration du spécificateur est int ; le déclarateur de la fonction est max(int a; int b, int c) et le bloc donnant le code de la fonction est {...}. La définition correspondante sous l'ancienne forme serait

    int max(a, b, c)
int a, b, c;
{
    /* ... */
}

où maintenant, le déclarateur est int max(a, b, c) et la liste de déclarations des paramètres est int a, b, c ;.

10.2 Les déclarations externes

Les déclarations externes indiquent les caractéristiques d'objets, de fonctions et d'autres identificateurs. Le terme "externe" fait référence à leur emplacement en dehors des fonctions et n'est pas directement lié au mot-clé extern ; la classe de stockage d'un objet défini de façon externe doit être laissée vide ou elle peut être spécifiée extern ou static.

Il peut exister plusieurs déclarations externes du même identificateur dans la même unité de traduction si elles correspondent au niveau du type et de l'édition de lien et s'il y a au plus une définition de l'identificateur.

Deux déclarations d'un objet ou d'une fonction sont supposées correspondre au niveau du type d'après les règles présentées au §8,10. De plus, si les déclarations différent parce qu'un type est une structure ou une union incomplète, ou un type énuméré incomplet (§8.3) et que l'autre est le type complet correspondant avec la même étiquette, on considère que les types correspondent. D'ailleurs, si la seule différence entre deux types est que l'un d'entre eux est un tableau complet et l'autre un tableau incomplet (§8.6.2), ils correspondent. Enfin, si un type spécifie une fonction sous l'ancienne forme et un autre spécifie une fonction sous la nouvelle forme, identique aux déclarations de paramètres près, ces types correspondent aussi.

Si la première déclaration externe d'une fonction ou d'un objet comprend le spécificateur static, l'identificateur a un lien interne ; sinon il a un lien externe. L'édition des liens est présentée au §11.2.

Une déclaration externe d'un objet est une définition si elle comporte un initialisateur. La déclaration d'un objet externe qui ne comporte pas d'initialisateur et qui ne contient pas le spécificateur extern est une définition potentielle. Si une unité de traduction comprend la définition d'un objet, toute définition potentielle est considérée comme une déclaration redondante. Si l'unité de traduction ne comprend aucune définition de l'objet, toute définition potentielle devient une définition unique avec une valeur initiale nulle.

Chaque objet doit avoir une et une seule définition. Pour les objets avec lien interne, cette règle s'applique séparément à chaque unité de traduction, parce que les objets avec lien interne sont uniques dans une unité de traduction donnée. Pour les objets avec lien externe, elle s'applique au programme entier.

      Bien que la règle de la définition unique soit formulée de façon quelque peu différente dans la première version du livre, elle est en fait identique à celle énoncée ici. Certaines implémentations l'assouplissent en généralisant la notion de déclaration potentielle. Dans la formulation de remplacement qui est habituelle sur les systèmes UNIX et qui est reconnue comme l'extension normale par la norme, toutes les définitions potentielles d'un objet avec lien externe, tout au long des unités de traduction d'un programme, sont considérées ensemble et non séparément dans chaque unités de traduction. Si un programme comprend quelque part la définition d'une fonction, alors ses définitions potentielles deviennent simplement des déclarations, mais s'il ne comprend pas sa définition, alors toutes ses définitions potentielles deviennent des définitions initialisées à zéro.

11. La portée et l'édition de liens

Il n'est pas nécessaire de compiler entièrement un programme en une fois : le texte source peut être conservé dans plusieurs fichiers contenant des unités de traduction et on peut charger en provenance de bibliothèques des sous-programmes précompilés. On peut réaliser les relations entre les fonctions d'un programme à la fois au moyen d'appels explicites et par la manipulation de données externes.

Par conséquent, il existe deux sortes de portée à considérer : premièrement, la portée lexicale d'un identificateur qui est la région du texte du programme à l'intérieur de laquelle les caractéristiques de l'identificateur sont connues ; et deuxièmement, la portée associée aux objets et fonctions avec lien externe, qui détermine les rapports entre les identificateurs des différentes unités de traduction compilées séparément.

11.1 La portée lexicale

Les identificateurs sont répartis en plusieurs classes de noms qui n'interfèrent pas les unes avec les autres ; on peut utiliser le même identificateur dans des buts différents, même dans la même portée, on l'utilise dans des classes de noms différentes. Ces classes sont : les objets, les fonctions, les noms de typedef et les constantes énumérées ; les étiquettes ; les étiquettes de structures, d'unions et d'énumérations ; et les membres de chaque structure et d'union prises individuellement.

      Ces règles sont différentes à plusieurs titres de celles décrites dans la première édition de ce manuel. Auparavant, les étiquettes ne possédaient pas leur propre classe ; les étiquettes de structures et d'unions avaient chacune une classe séparée, et dans certaines implémentations, les étiquettes d'énumérations en avaient une également ; le fait de mettre différentes sortes d'étiquettes dans la même classe de noms est une restriction nouvelle. Le plus important changement par rapport à 1a première édition est que chaque structure ou union crée une classe de nom séparée pour ses membres, si bien que l'on peut trouver le même nom dans plusieurs structures différentes. Cette règle est employé couramment depuis plusieurs années.

La portée lexicale de l'identificateur d'un objet ou d'une fonction dans une déclaration externe commence à la fin de sa déclaration et se poursuit jusqu'à la fin de l'unité de traduction dans laquelle elle se trouve. La portée d'un paramètre d'une définition de fonction commence au début du bloc qui définit la fonction et se termine à la fin de cette fonction ; la portée d'un paramètre dans une déclaration de fonction se termine à la fin de la déclaration. La portée d'un identificateur déclaré en tête d'un bloc commence à la fin de sa déclaration et se termine à la fin du bloc. La portée d'une étiquette est la totalité de la fonction dans laquelle elle apparaît. La portée d'une étiquette de structure, d'union ou d'énumération, ou celle d'une constante énumnérée, commence à l'endroit où elle figure dans un spécificateur de type, et se termine à la fin de l'unité de traduction (pour les déclarations externes) ou à la fin du bloc (pour les déclarations à l'intérieur d'une fonction).

Si un identificateur est explicitement déclaré en tête d'un bloc, y compris si ce bloc constitue une fonction, toute déclaration de l'identificateur en dehors du bloc est suspendue jusqu'à la fin du bloc.

11.2 L'édition de liens

A l'intérieur d'une unité de traduction, toutes les déclarations d'un même identificateur d'objet ou de fonction avec lien interne fait référence à la même chose et l'objet ou la fonction est unique pour cette unité de traduction. Toutes les déclarations d'un même identificateur d'objet ou de fonction avec lien externe font référence à la même chose et l'objet ou la fonction est partagé par le pro gramme entier.

Comme nous l'avons présenté au §10.2, la première déclaration externe d'un identificateur donne un lien interne à l'identificateur si le spécificateur static est utilisé, sinon elle lui donne un lien externe. Si la déclaration d'un identificateur à l'intérieur d'un bloc ne comprend pas le spécificateur extern, alors l'identificateur n'est pas lié et est unique pour la fonction. Si elle comprend extern, et si une déclaration externe de l'identificateur est active dans la portée entourant le bloc, l'identificateur est alors lié de la même façon que la déclaration externe et fait référence au même objet ou à la même fonction ; mais si aucune déclaration externe n'est visible, son lien est externe.

12. Le préprocesseur

Un préprocesseur effectue des substitutions de macros, une compilation conditionnelle et l'inclusion de fichiers nommés. Les lignes qui commencent par #, qui peut être précédé par un espace blanc, concernent ce préprocesseur. La syntaxe de ces lignes est indépendante du reste du langage ; on peut les trouver n'importe où et elles ont un effet qui s'étend (de façon indépendante de la portée) jusqu'à la fin de l'unité de traduction. Les limites des lignes sont importantes ; chaque ligne est analysée individuellement (voir tout de même le § AI 2.2 qui montre comment relier des lignes). Pour le préprocesseur, un lexème est un élément lexical quelconque du langage ou une suite de caractères donnant un nom de fichier comme dans la directive #include (§12.4) : de plus, tout caractère qui n'est pas défini par ailleurs est considéré comme un lexème. Cependant, l'effet des caractères d'espace blanc autre que l'espace et la tabulation horizontale est indéfini dans les lignes du préprocesseur. Le préprocesseur lui-même fonctionne en plusieurs phases logiques successives, dont certaines peuvent être rassemblées dans le cas d'une implémentation particulière.

  1. D'abord, les séquences d'échappement à trois caractères décrites au §12,1 sont remplacées par leurs équivalents. Si l'environnement du système le demande, des caractères de fin de ligne sont insérés entre les lignes du fichier source.
  2. Chaque occurrence du caractère backslash suivi d'un caractère de fin de ligne est supprimée, de façon à joindre les lignes (§12.2).
  3. Le programme est coupé en lexèmes séparés par des caractères d'espacement ; les commentaires sont remplacés par un simple espace. Alors, les directives du préprocesseur sont exécutées et les macros (§ §12.3-A12.10) sont développées.
  4. Les séquences d'échappement figurant dans les constantes de type caractère et de type chaîne (§§2.5.2-2.6) sont remplacées par leurs équivalents, les constantes de type chaîne adjacentes sont concaténées.
  5. Le résultat est traduit, puis lié avec les autres programmes et bibliothèques en rassemblant les programmes et les données nécessaires et en reliant les fonctions externes et les références d'objets à leurs définitions.

12.1 Les séquences d'échappement à trois caractères

Le jeu de caractères des programmes sources C est composé de caractères définis par l'ISO (ISO 644-1983 Invariant Code Set). Pour permettre aux programmes d'être représentés dans le jeu de caractères réduit, on remplace toutes les occurrences des séquences de trois caractères suivantes par le caractère unique correspondant. Ce remplacement est effectué avant tout traitement.

    ??= #       ??( [       ??< {
??/ \       ??) ]       ??> }
??' ^       ??! |       ??- -

Aucun autre remplacement n'est effectué.

Les séquences d'échappement à trois caractères sont des nouveautés de la norme ANSI.

12.2 Le raccordement des lignes

Les lignes se terminant par le caractère \ sont reliées en supprimant le \ et le caractère suivant de fin de ligne. Ceci est effectué avant la division en lexèmes.

12.3 La définition et l'expansion des macros

Une ligne de contrôle de la forme

    # define identificateur suite-de-lexèmes

demande au préprocesseur de remplacer les instances suivantes de l'identificateur par la suite de lexèmes indiquée ; les espacements en tête et en queue autour de la suite de lexèmes sont supprimés. Un second #define pour le même identificateur constitue une erreur à moins que la seconde suite de lexèmes soit identique à la première, où tous les espacements de séparation sont supposés équivalents.

Une ligne de la forme

    # define identificateur ( identificateur ,..., identificateur ) suite-de-lexèmes

où il n'y a pas d'espace entre le premier identificateur et la (, est une définition de macro avec paramètres donnés par la liste d'identificateurs. Comme avec la première forme, les espaces en tête et en queue entourant la suite de lexèmes sont supprimés, et la macro peut être redéfinie uniquement à l'aide d'une définition dans laquelle le nombre et l'orthographe des paramètres et la suite de lexèmes sont identiques.

Une ligne de contrôle de la forme

    # undef identificateur

demande que la définition de l'identificateur soit oubliée. Appliquer #undef à un identificateur inconnu ne constitue pas une erreur.

Quand une macro est définie avec la seconde forme, les instances textuelles ultérieures de l'identificateur de la macro suivies de caractères d'espacement éventuels, puis d'une suite de lexèmes séparés par des virgules entre parenthèses, constituent des appels de la macro. Les arguments de l'appel sont les suites de lexèmes séparées par des virgules ; les virgules se trouvant entre guillemets ou protégées par des parenthèses imbriquées n'effectuent pas de séparation d'arguments. Pendant l'assemblage, les arguments ne sont pas développés. Le nombre d'arguments de l'appel doit correspondre au nombre de paramètres de la définition. Une fois que les arguments sont isolés, les caractères d'espacement en tête et en queue sont supprimés. Alors, la suite de lexèmes résultant de chaque argument est substituée à chaque occurrence (ne figurant pas entre guillemets) de l'identificateur du paramètre correspondant dans la suite de lexèmes de remplacement de la macro. A moins que le paramètre de la séquence de remplacement soit précédé de #, ou précédé ou suivi de ##, les lexèmes de l'argument sont examinés à chaque appel de la macro et développés en conséquence avant insertion.

Deux opérateurs spéciaux influent sur le processus de remplacement Premièrement, si une occurrence d'un paramètre de la séquence de lexèmes de remplacement est immédiatement précédé de #, des guillemets (") sont placés autour du paramètre correspondant et alors le # et l'identificateur du paramètre sont tous les deux remplacés par l'argument entre guillemets. Un caractère \ est inséré avant chaque caractère " ou \ qui figure autour ou à l'intérieur d'une constante de type chaîne ou de type caractère dans l'argument.

Deuxièmement, si la suite de lexèmes de la définition pour les deux types de macro contient l'opérateur If, alors juste après le remplacement des paramètres, chaque ## est supprimé en même temps que tout espacement de chaque côté de telle façon à concaténer les lexèmes adjacents et former un nouveau lexème. L'effet est indéterminé si des lexèmes incorrects sont produits ou si le résultat dépend de l'ordre du traitement des opérateurs ##. De même, on ne peut pas trouver ## au début ou à la fin d'une suite de lexèmes de remplacement.

Dans les deux sortes de macros, la suite de lexèmes de remplacement est parcourue à nouveau de façon répétitive pour des identificateurs défini s en plus. Cependant, une fois qu'un identificateur donné a été remplacé dans un développement donné, il n'est pas remplacé s'il reprend la précédente valeur pendant ce rebal aya ge ; au lieu de cela, il reste inchangé.

Même si la valeur finale d'un développement de macro commence par #, elle n'est pas considérée comme une directive du préprocesseur.

      Les détails du processus de développement des macros sont décrits plus précisément dans l'édition de la norme ANSI que dans la première version. La modification la plus importante est l'apport des opérateurs # et ##, qui permettent la mise entre guillemets et la concaténaùon. Certaines nouvelles règles sont bizarres, en particulier celles qui concernent la concaténation. (voir l'exemple ci-dessous)

Par exemple, on peut utiliser cette fonctionnalité pour les "constantes manifestes" comme

    #define TAILLETAB 100
    int table[TAILLETAB];

La définition

    #define ABSDIFFI(a,b) ((a)>(b) ? (a)-(b) : (b)-(a))

définit une macro qui retourne la valeur absolue de la différence de ses arguments. Contrairement à une fonction qui effectuerait la même chose, les arguments et la valeur de retour peuvent être de n'importe quel type arithmétique et même de type pointeur. De plus, les arguments qui peuvent avoir des effets de bord, sont évalués deux fois, une fois pour le test et une fois pour produire le résultat.

Etant donné la définition.

    #define fichtemp(rep) #rep "/%s"

l'appel de la macro fichtemp (/usr/tmp) produit

    "/usr/tirp" "/%s"

qui sera ensuite concaténée en une seule chaîne de caractères. Après la directive

    #define cat(x,y) x ## y

l'appel cat(var,123) produira var123.

Cependant l'appel cat(cat(1,2),3)) est indéfini : la présence de ## empêche de développer les arguments de l'appel extérieur. Par conséquent, celui-ci produira la chaîne de lexèmes

    cat(1,2)3

et )3 (la concaténation du dernier lexème du premier argument et du premier lexème du second) n'est pas un lexème valide. Si un second niveau de définition est introduit,

    #define xcat(x,y) cat(x,y)

les choses fonctionnent sans problème ; xcat(xcat(1,2),3) produira 123 parce que le développement de xcat ne met pas en jeu l'opérateur ##.

De même, ABSDIFF(ABSDIFF(a,b),c) produira le résultat totalement développé attendu.

12.4 L'inclusion de fichier

Une ligne de contrôle de la forme

    # include <nom-de-fichier>

provoque le remplacement de cette ligne par le contenu total du fichier nom-de-fichier. Le nom de fichier ne doit pas comporter les caractères > ou de fin de ligne et l'effet est indéfini s'il contient un des caractères ", ', \ ou /*. Le fichier indiqué est recherché dans une suite d'endroits dépendant de l'implémentation.

De même, une ligne de contrôle de la forme

    # include "nom-de-fichier"

recherche d'abord le fichier source original associé (une expression qui dépend délibérément de l'implémentation) et, en cas d'échec, effectuera la même recherche que pour la première forme. L'effet de l'utilisation de ', \ ou /* dans le nom de fichier reste indéfini mais > est autorisé.

Finalement, une directive de la forme

# include suite-de-lexèmes

qui ne correspond pas aux formes précédentes sera interprétée en développant la suite de lexèmes comme un texte normal ; il doit résulter une des deux formes <...> ou "...", et la directive est alors traitée comme décrit précédemment.

On peut imbriquer les fichiers #include.

12.5 La compilation conditionnelle

On peut compiler de façon conditionnelle différentes parties d'un programme d'après la syntaxe schématique suivante

    condition-préprocesseur :

      ligne-condition texte parties-sinon-condition partie-sinon-opt # endif

    ligne-condition :

      # if expression-constante
      # ifdef identiicateur
      # ifndef identificateur

    parties-sinon-condition :

      ligne-sinon-condition texte
      parties-sinon-condition-      opt

    ligne-sinon-condition :

      # elif expression-constante

    partie-sinon :

      ligne-sinon texte

    ligne-sinon texte:

      # else

Chacune des directives (ligne-condition, partie-sinon-condition, ligne-sinon et #endif) se trouve seule sur une ligne. Les expressions constantes des lignes #if et ensuite #elif sont évaluées dans l'ordre jusqu'à ce qu'on trouve une expression avec une valeur non nulle ; le texte qui suit une ligne avec une valeur nulle est détruit. Le texte qui suit une ligne comportant une directive réussie est traité normalement. Un "texte" fait ici référence à n'importe quel bloc contenant des lignes du préprocesseur ne faisant pas partie de la structure conditionnelle ; il peut être vide. Une fois qu'on a trouvé une ligne #if ou #elif réussie et que son texte a été traité, les lignes #elif et #else suivantes ainsi que leur texte sont supprimées. Si toutes les expressions sont nulles et qu'il y a un #else, le texte suivant le #else est traité normalement. Le texte contrôlé par des branches conditionnelles inactives est ignoré à part la vérification de l'imbrication des blocs conditionnels.

L'expression constante dans #if et #elif est soumise au remplacement de macro ordinaire, D'ailleurs, toute expression de la forme

    defined identificateur

ou

    defined ( identificateur )

est remplacée, avant le balayage des macros, par 1L si l'identificateur est défini dans le préprocesseur et sinon par 0L. Finalement, chaque constante entière est considérée être suffixée par 1L de telle façon que toute valeur arithmétique soit considérée long ou unsigned long.

L'expression constante résultante (§7.19) est restrictive : elle doit être entière et ne peut contenir sizeof, un "cast" ou une constante énumérée.

Les lignes de contrôle

# ifdef identificateur
# ifndef identificateur

sont respectivement équivalentes à

# if defined identificateur
# if ! defined identificateur

      #elif est nouveau par rapport à la première édition bien que cette directive ait été disponible avec certains préprocesseurs. L'opérateur du préprocesseur defined est également nouveau.

12.6 Le contrôle des lignes

Pour aider d'autres compilateurs générant des programmes C, une ligne parmi les formes suivantes

    # line constante "nom-de-fichier"
    # line constante

permet au compilateur de savoir, dans un but de diagnostic d'erreurs, que le numéro de la ligne source suivante est donné par la constante entière décimale, et le nom du fichier courant en entrée par l'identificateur. Si le nom de fichier entre guillemets est absent, le nom précédent n'a pas changé. Les macros de la ligne sont développées avant d'être interprétées.

12.7 La génération des erreurs

Une ligne du préprocesseur de la forme

    # error suite-de-lexèmes-opt

provoque l'écriture par le processeur d'un message de diagnostic qui contient la suite de lexèmes.

12.8 Les pragmas

Une ligne de contrôle de la forme

    # pragma suite-de-lexèmes-opt

provoque l'exécution par le processeur d'une action dépendant de l'implémentation. Un pragma non reconnu est ignoré.

12.9 La directive nulle

Une ligne du préprocesseur de la forme

    #

n'a aucun effet.

12.10 Les noms prédéfinis

Plusieurs identificateurs sont prédéfinis et se développent pour produire des informations spéciales. Leurs définitions, ainsi que l'opérateur d'expression du préprocesseur del ined, ne peuvent pas être supprimées ni modifiées.

    _ _LINE_ _

    Une constante décimale contenant le numéro de ligne courante du source.

    _ _FILE_ _

    Une constante de type chaîne contenant le nom du fichier en cours de compilation.

    _ _DATE_ _

    Une constante de type chaîne contenant la date de compilation sous la forme "Mmm:jj:aaaa" .

    _ _TIME_ _

    Une constante de type chaîne contenant l'heure de compilation sous la forme "hh:mm:ss".

    _ _STDC_ _

    La constante 1. Cet identificateur est supposé valoir 1 par définition uniquement dans des implémentations conformes à la norme.

      #error et #pragma sont des nouveautés de la norme ANSI ; les macros prédéfinies du préprocesseur sont nouvelles, mais certaines étaient déjà disponibles dans certaines implémentations.


Vu dans ... http://www.mysti2d.net/polynesie/SIN/08/LangageC/files/GlossairelangageC.html

Glossaire langage C

Dernière mise à jour : 2 janvier 2011

Affectation

  • En C, l'opérateur d'affectation de base est noté =
    Cet opérateur binaire est utilisable dans toute expression dont l'opérande gauche possède une adresse (on parle de Lvalue [Left value]). Cela exclut donc toutes les constantes (constantes numériques, résultats d'expressions arithmétiques, pointeurs constants tels que noms de tableaux et noms de fonctions)
  • On parle d'opérateur à effet de bord car il modifie la valeur de l'emplacement en mémoire associé.
    Une expression telle que x = 5 dans laquelle la variable x est de type int a comme valeur 5 et affecte à la variable x la valeur 5.
  • Il existe par ailleurs des opérateurs d'affectation étendue : +=, -=, *=, /=, %=, ^=, |=, &=, |=, &=, <<= et >>=.
    De manière générale, une expression « a a= b » est équivalente à « a = a a (b) »

Blocs (ou instructions composées)

  • Il s'agit là de regrouper une séquence d'instructions pour en permettre l'utilisation comme s'il s'agissait d'une instruction élémentaire.
  • Pour constituer un bloc, il suffit de faire précéder la première instruction qui lui appartient d'une accolade ouvrante { et de faire suivre la dernière (c'est-à-dire le ; qui en marque la fin) d'une accolade fermante }
  • Des déclarations ou définitions de variables peuvent par ailleurs être ajoutées n'importe où dans le bloc (mais la lisibilité du programme plaide pour que ces déclarations et déclarations soient placées en début de bloc, certains compilateurs l'imposant) : la portée de ces identificateurs (la partie du code où elles seront visibles) sera limitée à la suite des instructions qui les suivent et qui appartiennent au bloc.
  • Les blocs peuvent être emboîtés les uns dans les autres : autrement dit on peut définir un bloc à l'intérieur d'un autre (le bloc extérieur).
    La déclaration d'une variable faite dans un bloc est visible dans tous les blocs définis à l'intérieur de ce bloc, à moins qu'une nouvelle définition de cette variable ne soit faite à l'intérieur de ce bloc : la règle est qu'une nouvelle définition masque la définition extérieure.
  • L'exemple suivant illustre avec des couleurs l'emboîtement de blocs et la visibilité des identificateurs homonymes :

--> cat bloc.c
#include <stdio.h>
#include <stdlib.h>
int main( )
{ // début du bloc 1
int a = 1;
{ // début du bloc 1.1
printf("%d\n", a);
int a = 10;
printf("%d\n", a);
{ // début du bloc 1.1.1
printf("%d\n", a);
int a = 100;
printf("%d\n", a);
a ++;
printf("%d\n", a);
} // fin du bloc 1.1.1
a ++;
printf("%d\n", a);
} // fin du bloc 1.1
{ // début du bloc 1.2
printf("%d\n", a);
int a = 1000;
printf("%d\n", a);
a ++;
printf("%d\n", a);
} // fin du bloc 1.2
printf("%d\n", a);
return EXIT_SUCCESS; // return 0
} // fin du bloc 1

L'exécution de ce programme fournit les résultats suivants :

--> ./bloc
1
10
10
100
101
11
1
1000
1001
1

break

  • L'instruction « break; » est utilisable à l'intérieur d'un switch ou d'une boucle (de l'une quelconque des trois formes).
  • Elle provoque la sortie de cette instruction, c'est-à-dire la poursuite de l'exécution à l'instruction suivante.

Comparaison (opérateurs relationnels)

Ces opérateurs permettent de comparer des valeurs numériques ou des pointeurs :

  • == : égalité
  • != : inégalité
  • < : strictement plus petit
  • > : strictement plus grand
  • <= : plus petit ou égal
  • >= : plus grand ou égal

const

  • A côté de la possibilité de définir des constantes (au niveau du préprocesseur avec la directive #define ou au niveau du compilateur, pour des constantes entières, avec le concept d' énumération ), il est dorénavant (avec la norme ANSI) possible de définir des variables non modifiables.
  • Ces variables doivent être :
  • initialisées lors de leur définition;
  • qualifiées constantes avec le mot clé const.

  • Le mot clé const peut être placé avant ou après le nom du type comme dans

const int a = 1;
int const b = 2;

De telles définitions de m et n entrainent une erreur lors de la compilation pour des tentatives de modification des valeurs de ces variables :

a ++; // error: increment of read-only variable 'a'
b = 2; // error: assignment of read-only variable 'b'

Si les deux écritures sont équivalentes dans le cas de variables de type classique (types de base, structures ou unions), les choses sont un peu différentes dans le cas de la définition de variables de type pointeur :

  • s'il s'agit de définir un pointeur constant, on écrira :

int a, b;
int * const ptr = &a;
Le fait que ptr soit constant, interdit dans la suite du programme toute instruction modifiant le pointeur :
ptr = &b; // erreur de compilation: assignment of read-only variable 'ptr'

  • s'il s'agit de définir un pointeur sur quelque chose de constant, on écrira :

int a, b;
int const * ptr = &a;

Le fait que l'espace pointé par ptr soit déclaré constant, interdit dans la suite du programme l'instruction

*ptr = 12; // erreur de compilation: assignment of read-only location
Il est intéressant d'observer ce qui se passe dans la séquence suivante : un simple avertissement (warning) signale que le pointeur référence une variable qualifiée const, ce que la définition du pointeur ne spécifie pas :
int const a;
int *ptr=&a; // warning:initialization discards qualifiers from pointer target type
Un exécutable est donc produit. L'exécution de l'instruction suivante, qui modifie indirectement la variable supposée constante n'a donné lieu à aucune erreur (mais un tel comportement non souhaitable n'est pas garanti sur tout système et risque de conduire sur certains à un arrêt brutal de l'exécution pour violation mémoire) :
*ptr = 1000;
printf("%d\n", a);

  • s'il s'agit de définir un pointeur constant sur quelque chose de constant, on pourra par exemple écrire :
    const int a;
    int const * const ptr = &a;

continue

  • L'instruction « continue; » est utilisable à l'intérieur d'une boucle (de l'une quelconque des trois formes).
  • Elle provoque le passage à l'itération suivante de cette boucle.

Cela signifie que:

  • pour une boucle while ou do, le passage au test contrôlant la boucle, puis le cas échéant (si le résultat obtenu a comme valeur logique Vrai) nouvelle exécution du corps de la boucle;
  • pour une boucle for(e1; e2; e3) l'évaluation immédiate de l'expression e3, puis celle de l'expression e2. Si la valeur obtenue a comme valeur logique Vrai le corps de la boucle est exécuté de nouveau et sinon on sort de la boucle.

Décrémentations (autodécrémentations)

L'opérateur -- est un opérateur unaire (un argument) applicable à un argument adressable (Lvalue).
C'est un opérateur à effet de bord (modifiant la valeur de l'espace mémoire correspondant à son argument) qui est utilisable sous deux formes :

  • en forme préfixée (pré-décrémentation) : la valeur de la variable x étant 4, l'expression --x
  • décrémente la valeur de la variable x dont la nouvelle valeur est donc 3 (effet de bord)
  • a comme valeur la nouvelle valeur de x, c'est-à dire 3
  • en forme postfixée (post-décrémentation) : la valeur de la variable x étant 4, l'expression x--
  • décrémente la valeur de la variable x dont la nouvelle valeur est donc 3 (effet de bord)
  • a comme valeur l'ancienne valeur de x, c'est-à 4

do ... while

  • La forme générale de la boucle est :

do
{
suite_instructions
}
while (expression)

  • Cette structure de contrôle est assez semblable à la structure while dans le sens où elle permet d'itérer l'exécution d'une suite d'instructions tant qu'une certaine condition est vraie.
  • La différence est dans le fait que le test contrôlant l'itération est fait en fin d'itération et non en début comme c'est le cas pour les boucles while.
    Une conséquence en est que le corps d'une boucle do ... while est exécutée au moins une fois.

Effet de bord

On parle d'effet de bord dans le cas d'un opérateur (par exemple =, ++ ou -=) modifiant l'un de ses opérandes ou d'une fonction qui modifie la valeur d'une variable globale ou d'une zone pointée par un de ses paramètres.

Expressions composées

L'opérateur « , » permet de composer deux expressions.

Ainsi, une expression de la forme

exp1,exp2

est une expression dont l'évaluation entraîne successivement :

  • l'évaluation de exp1;
  • l'évaluation de exp2;
  • c'est cette dernière évaluation qui donne le type et la valeur à l'expression composée.

Ainsi l'évaluation de l'expression

x = (y = 4, z = 2 * y)

entraîne l'évaluation successive des expressions

  • « y = 4 » de valeur 4 (avec effet de bord affectant 4 à la variable y),
  • «, z = 2 * y » de valeur 8 (avec effet de bord affectant 8 à z).
    Cette valeur est celle de l'expression composée et elle est finalement affectée à la variable x.

Expressions conditionnelles

  • Etant données trois expressions exp1, exp2 et exp3, on en déduit une expression conditionnelle par la forme

exp1?exp2:exp3

dont l'évaluation est la suivante :

  • évaluation de l'expression exp1
  • si la valeur logique de cette évaluation est vraie, évaluation de l'expression exp2, et la valeur obtenue est la valeur de l'expression conditionnelle;
  • sinon, évaluation de l'expression exp3, et la valeur obtenue est la valeur de l'expression conditionnelle.

  • Quelques exemples d'expressions conditionnelles :
  • l'expression « x >= 0 ? x : -x » calcule la « valeur absolue de x »;
  • si les valeurs de a et b sont respectivement 13 et 11, l'expression

(a > 5) ? ((b <= 150) ? 3.25 : 9) : -13.0

a pour valeur 3.25.

Fonctions

Définition de fonction

La définition d'une fonction est constituée de

  • l'en-tête de la fonction constituée successivement de :
  • éventuellement le qualificatif static.
    S'il est présent, la définition de la fonction ne sera visible que dans le fichier où elle est définie.
  • le type de la valeur retournée par la fonction
  • le nom de la fonction
  • une parenthèse ouvrante (
  • une liste de déclarations de paramètres (chacune conteint un type et un nom de paramètre formel) séparées par une virgule
  • une parenthèse fermante )
  • le bloc de définition de la fonction, constitué comme tout bloc, dans l'ordre de :
  • une accolade ouvrante {
  • des définitions de variables qui seront locales au bloc
  • une suite d'instructions et de blocs
  • une accolade fermante }

Une seule définition peut être attachée à un nom de fonction. Il n'y a pas la possibilité, comme en C++ ou Java, de donner plusieurs définitions de fonctions sous le même nom mais différant par leur signature (nombre ou type des paramètres).

Appel de fonction

L'évaluation et la transmission des paramètres depuis le module appelant vers le module appelé possèdent les propriét&eacutes importantes suivantes :

  • la norme ne dit rien sur l'ordre dans lequel l'évaluation des paramètres est réalisé : il dépend des implémentations.

Cela signifie qu'il faut bannir les effets de bord dans les expressions utilisées dans les paramètres d'appel des fonctions.

  • le passage des paramètres est réalisé par valeur.

Cela signifie que pour chacun des paramètres, une variable locale (portant le nom du paramètre formel) est créée sur la pile. Cette variable est initialisée avec la valeur du paramètre d'appel correspondant.

Prototype d'une fonction (déclaration d'une fonction)

  • Le prototype d'une fonction en donne une description en vue de son utilisation sans qu'on en connaisse la définition complète. Celle-ci peut être donnée plus loin dans le code ou faire partie d'un code dans un autre fichier, voire être disponible sous forme binaire dans une bibliothèque (dans ce dernier cas, le prototype est normalement donné dans un fichier d'interface de la bibliothèque [un fichier d'extension .h] et le source d'un programme utilisant la bibliothèque est supposé inclure ce fichier [#include]).

Le prototype d'une fonction doit normalement être décrit avant sa première utilisation (voir cependant la remarque ).

Un tel prototype permet au compilateur de « vérifier » que l'utilisation de la fonction est conforme à sa définition.

  • A ces fins, un prototype est constitué de :

  • de manière optionnelle, le mot-réservé extern ;
  • le type de valeur retournée par la fonction ;
  • le nom de la fonction ;
  • les types des paramètres de la fonction.

Il est suivi d'un point virgule et non du corps de la fonction (contenant les instructions à exécuter), et ne nomme pas a priori les paramètres (s'il en contient nous parlerons de prototype étendu).

  • Remarques :
  • la définition d'une fonction en constitue de facto un prototype dans ce qui la suit dans le fichier où elle est définie.
  • il existe un prototype par défaut pour les fonctions. Ce type est automatiquement attribué par le compilateur C à une fonction lors du premier appel si un prototype ne lui pas été associé préalablement.
  • le type par défaut de la valeur retournée par une fonction est int
  • la liste des paramètres par défaut est vide (attention cela n'est pas équivalent au type void : une telle fonction ne peut avoir de paramètre lors d'un appel). Avec ce prototypage par défaut (qui n'en est pas vraiment un), tout appel de la fonction sera alors correct, avec n'importe quel nombre de paramètres et n'importe quel type de ces paramètres.

Ce petit exemple illustre ce que nous venons de dire :
--> cat prototype.c
#include <stdio.h>
#include <stdlib.h>
void g(void){
printf("Bonjour\n");
}
int main( ) {
float x = f(3, 5);
g(3);
printf("%f\n", x);
return EXIT_SUCCESS;
}
int f(int x, int y, int z){
return x + y + z;
}
--> gcc prototype.c
prototype.c: In function 'main':
prototype.c:6: warning: implicit declaration of function 'f'
prototype.c:7: error: too many arguments to function 'g'
-->

  • Des exemples de prototypes :

/* deux paramètres de type int et retour d'un int */
int somme(int, int);
/* un paramètre de type size_t et retour d'un pointeur sur char */
void *malloc (size_t);

for

  • Il s'agit de la forme la plus générale de boucle dont la syntaxe est la suivante :

for(exp_initialisation; exp_condition; exp_modification)
instruction

dans laquelle :

  • « exp_initialisation; » est une instruction simple réalisant typiquement une série d'initialisations comme par exemple
  • i = 1;
  • i = 0, j = 2, k = n;
  • « exp_condition » est une expressison dont la valeur logique servira à contrôler la boucle comme par exemple :
  • i < n;
  • i > j + k
  • i < m && j + k <= n && t[i][j+k] != x;
  • « exp_modification; » est une instruction simple réalisant typiquement une série de modifications de variables entrant utilisées dans l'expression utilisée pour contrôler la boucle.
    Des exemples de telles instructions sont :
  • i++;
  • i++, j += 2, k -= 3;

? Une telle boucle est équivalente à la boucle while suivante :

exp_initialisation;
while(exp_condition)
{
instruction
exp_modification;
}

Identificateur

  • Il s'agit d'une suite de caractères commençant par une lettre (minuscule ou majuscule, caractère « _ »), les caractères suivants étant des lettres ou des chiffres.
  • La longueur d'un identificateur n'est pas limitée.

if

Il s'agit d'une structure de contrôle permettant de réaliser des tests et d'exécuter des instructions ou blocs différents selon la valeur de ces tests.
Dans ce qui suit instruction désigne soit une instruction simple, soit un bloc (délimité par des accolades) et expression désigne une expression quelconque dont on considérera la valeur logique déduite de sa valeur..

  • forme simple :

if (expression)
instruction

L'instruction n'est exécutée que si la valeur logique de l'expression est vraie

  • forme générale :

if (expression)
instruction1
else
instruction2

L'expression est évaluée : si sa valeur logique est vraie, l'instruction1 est exécutée et sinon c'est l'instruction2 qui est exécutée.

  • imbrication :
    plusieurs if peuvent être imbriqués.
    Dans ce cas, chaque else est associé au if le plus proche pas encore associé à un autre else.

Dans la séquence suivante, les if et else de même couleur sont associés :

if(a)
if(b)
x = 3;
else if(c)
x = 4;
else {
if(d)
x = 5;
}
else
x = 6;

Incrémentations (autoincrémentations)

L'opérateur ++ est un opérateur unaire (un argument) applicable à un argument adressable (Lvalue).
C'est un opérateur à effet de bord (modifiant la valeur de l'espace mémoire correspondant à son argument) qui est utilisable sous deux formes :

  • en forme préfixée (pré-incrémentation) : la valeur de la variable x étant 4, l'expression ++x
  • incrémente la valeur de la variable x dont la nouvelle valeur est donc 5 (effet de bord)
  • a comme valeur la nouvelle valeur de x, c'est-à dire 5
  • en forme postfixée (post-incrémentation) : la valeur de la variable x étant 4, l'expression x++
  • incrémente la valeur de la variable x dont la nouvelle valeur est donc 5 (effet de bord)
  • a comme valeur l'ancienne valeur de x, c'est-à 4

Instructions élémentaires (ou simples)

Toute instruction élémentaire se déduit d'une expression en lui ajoutant le caractère « ; »

Le caractère « ; » fait donc partie de l'instruction et en marque la terminaison.
Ce n'est pas, comme c'est le cas dans d'autres langages (Pascal par exemple), un opérateur de composition ou un séparateur d'instructions.

Remarque : une instruction telle que « i + 1; » est correcte mais ne fait rien puisqu'elle ne modifie rien en mémoire (pas d'effet de bord).

Instruction vide

L'instruction « ; » réduite à un point virgule est l'instruction vide. Elle est utilisable par exemple comme corps de boucle ou dans une instruction conditionnelle en partie if ou else.

main

La fonction main constitue le « point d'entrée » dans un binaire exécutable, c'est-à-dire qu'au lancement du programme, c'est cette fonction qui est automatiquement appelée.

Ainsi :

  • un binaire exécutable (génériquement appelé a.out) ne peut être construit (de manière ultime par l'éditeur de liens) que si une fonction main est définie dans l'application;
  • au lancement de l'application, la fonction main reçoît des informations lors de ce lancement sous forme de paramètres;
  • la fonction main peut « faire remonter » une valeur entière lors de sa terminaison en particulier au travers d'un appel de return ou via un appel à la primitive exit .
    C'est pourquoi la valeur du type retourné attendu est int.
    Au niveau du système, cette valeur définit le code de retour du processus à sa terminaison.

Arguments/paramètres de la fonction main : la fonction est normalement appelée sous deux formes (c'est ce qu'attend par exemple le compilatuer gcc) :

  • sans paramètre, c'est-à-dire sous la forme « main( ) »;
  • avec deux paramètres, c'est-à-dire sous la forme « main(argc, argv) » où :
  • argc est de type int;
  • argv est un tableau de pointeurs sur caractère, c'est-à-dire déclaré sous la forme « char *argv[] ».
    L'idée est que, lorsque l'application est lancée, elle reçoît de l'appelant (en l'occurrence le système d'exploitation) des informations.

Consultation d'un programme parcourant les paramètres.

Opérateurs logiques (booléens)

Ils permettent de composer de composer les valeurs logiques d'expression et par là de réaliser des tests généraux.

Quatre opérateurs sont disponibles :

  • un opérateur unaire, la négation qui est notée !
  • trois opérateurs binaires :
  • la conjonction (ET) notée &&
  • la disjonction (OU) notée ||
  • le XOR (OU exclusif) noté ^

Priorité des opérateurs

Il existe 15 niveaux de priorité.

Pour chaque niveau, l'associativité est réalisée de gauche à droite ou de droite à gauche comme indiqué.
A titre d'exemple, l'associativité des opérateurs arithmétiques additifs est réalisée de gauche à droite et ces opérateurs sont moins prioritaires que les opérateurs arithmétiques mutiplicatifs.
Cela signifie que l'expression
3-4+5*6-7-8+1
est évaluée comme
(((((3-4)+(5*6))-7)-8)+1)
et a donc 16 comme valeur.

L'associativité des opérateurs d'affectation étant de droite à gauche et l'addition étant prioritaire par rapport aux affectations, l'expression
a=b+=b*=c=2+b
est évaluée comme
a=(b+=(b*=(c=(2+b))))
Si b vaut initialement 3, après évaluation de cette expression, les nouvelles valeurs des variables a, b et c sont respectivement (par effet de bord) 30, 30 et 5.

Premier niveau : indexation, fonction, structure

Opérateur | Notation | Exemples | Associativité
indexation | [ ] | t[i][j] | de gauche à droite
accès aux champs | . -> | c.champ pointeur->champ | de gauche à droite
appel de fonction | ( ) | f(a, b, 3) | de gauche à droite

Deuxième niveau : opérateurs unaires

Opérateur | Notation | Exemples | Associativité
négation logique | ! | !(x == 4) !x | de droite à gauche
négation bit à bit | ˜ | ˜ent !x | de droite à gauche
incrémentation | ++ | ++x x++ | de droite à gauche
décrémentation | -- | --x x-- | de droite à gauche
moins unaire | - | -(x + 1) | de droite à gauche
coercition (cast) | (type) | (char *) (float) | de droite à gauche
taille | sizeof | sizeof(int) sizeof(n) | de droite à gauche
référencement (adresse de) | & | &n | de droite à gauche
déréférencement (valeur pointée) | * | *ptr | de droite à gauche

Troisième niveau : opérateurs arithmétiques multiplicatifs

Opérateur | Notation | Exemples | Associativité
mutiplication | * | 5 * x | de gauche à droite
division | / | x / y | de gauche à droite
modulo | % | x % 5 | de gauche à droite

Quatrème niveau : opérateurs arithmétiques additifs

Opérateur | Notation | Exemples | Associativité
addition | + | x + 8 | de gauche à droite
soustraction | - | a - b | de gauche à droite

Cinquième niveau : opérateurs de décalage

Opérateur | Notation | Exemples | Associativité
décalage à gauche | << | a << 4 | de gauche à droite
décalage à droite | >> | x >> n | de gauche à droite

Sixième niveau : opérateurs relationnels (plus grand et plus petit)

Opérateur | Notation | Exemples | Associativité
plus grand | > | a > b | de gauche à droite
plus grand ou égal | >= | a >= 5 | de gauche à droite
plus petit | < | y < a | de gauche à droite
plus petit ou égal | <= | a <= b | de gauche à droite

Septième niveau : opérateurs relationnels (égal, différent)

Opérateur | Notation | Exemples | Associativité
égalité | == | (x + 1) == (y + 2) | de gauche à droite
négalité | != | z != 5 | de gauche à droite

Huitième niveau : ET bit à bit

Opérateur | Notation | Exemples | Associativité
ET bit à bit | & | x & 0x3456AF01 | de gauche à droite

Neuvième niveau : XOR bit à bit (OU exclusif)

Opérateur | Notation | Exemples | Associativité
XOR bit à bit | ^ | x ^ y | de gauche à droite

Dixième niveau : OU bit à bit (OU exclusif)

Opérateur | Notation | Exemples | Associativité
OU bit à bit | | | x | 0234 | de gauche à droite

Onzième niveau : ET logique

Opérateur | Notation | Exemples | Associativité
conjonction logique (ET) | && | a > b) && (z < 5) | de gauche à droite

Douzième niveau : OU logique

Opérateur | Notation | Exemples | Associativité
disjonction logique (OU) | || | (a != 0) || (b / a > 5) | de gauche à droite

Treizième niveau : expression conditionnelle

Opérateur | Notation | Exemples | Associativité
expression conditionnelle | ? : | (x>0)?x:-x | de droite à gauche

Quatorzième niveau : affectations

Opérateur | Notation | Exemples | Associativité
affectation simple | = | a = 5 | de droite à gauche
affectations étendues (arithmétiques) | += -= *= /= %= | a+=5 a*=(b+1) | de droite à gauche
affectations étendues (opérations bit à bit) | &= ^= |= | a&=b a^=0x789A | de droite à gauche
affectations étendues (décalages) | <<= >>= | a<<=5 | de droite à gauche

Quinzième niveau : composition d'instructions

Opérateur | Notation | Exemples | Associativité
composition d'instructions | , | a=2,b=a+3,z+=5 | de gauche à droite

switch

Souvent appelée aiguillage, cette structure permet, après évaluation d'une expression, de « se brancher » (et donc d'y poursuivre l'exécution) en un point (une étiquette) correspondant à la valeur de l'expression.

La syntaxe de la structure switch est la suivante (chaque suite_instructions peut éventuellement être vide et consiste sinon en la concaténation d'instructions, dont l'instruction « break; ») :

switch ( expression_entière )
{
case constante1 : suite_instructions1
case constante2 : suite_instructions2
........
case constanteN : suite_instructionsN
default : suite_instructions
}
// point de reprise d'exécution en cas de break dans le switch

La dernière alternative, notée default correspond au cas par défaut (si la valeur de l'expression ne correspond à aucun des cas prévus).
Elle est facultative. Cependant, pour des raisons de lisibilité des programmes, son usage est conseillé, quitte à lui associer une liste d'instructions vide;

Les étiquettes de chacun des cas sont des expressions constantes (que le compilateur doit donc pouvoir calculer).

la sémantique de cette structure de contrôle est la suivante :

  • L'expression (à valeur entière) contrôlant l'instruction est évaluée;
  • l'exécution reprend sur la liste d'instructions repérée par l'« étiquette » associée à la valeur de l'expression.
  • l'exécution se poursuit en séquence. Elle ne se limite pas à la liste des instructions allant jusqu'à l'alternative suivante.
    Si on souhaite interrompre l'enchaînement des instructions, il faut utiliser l'instruction « break; » qui provoque un saut à l'instruction suivant l'instruction switch.

un exemple utilisant un switch pour imprimer un message différent selon la nature du caractère contrôlant l'instruction :

switch(c) {
case 'A': case 'E': case 'I': case 'O': case 'U': case 'Y':
printf("Le caractere lu est une voyelle\n");
break;
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
printf("Le caractere lu est un chiffre\n");
break;
case 'B': case 'C': case 'D': case 'F': case 'G':
case 'H': case 'J': case 'K': case 'L': case 'M':
case 'N': case 'P': case 'Q': case 'R': case 'S':
case 'T': case 'V': case 'W': case 'X': case 'Z':
printf("Le caractere lu est une consonne\n");
break;
default :
printf("Le caractere lu n'est ni une lettre, ni un chiffre\n");
// break;
}

Le type void

Il a été introduit avec la norme ANSI du langage pour :

  • permettre de définir des fonctions ne renvoyant pas de valeurs (telles que les procédures ou sous-programmes proposés dans d'autres langages);
  • permettre de spécifier qu'une fonction ne possède pas de paramètres;
  • permettre de spécifier l'utilisation de pointeurs génériques compatibles avec les pointeurs de tout type.

Le type void est un type incomplet (de taille non définie) : il correspondant à une absence de type et par suite :

  • il est interdit de définir ou déclarer des variables de type void

--> cat essai.c
int main(){void a; }
--> gcc a.c
essai.c: Dans la fonction « main »:
essai.c:2: error: variable or field `a' declared void
-->

  • par contre il est possible (et c'est en grande partie pour cela que ce type a été introduit) de définir des pointeurs sur void.
    Il convient d'interpéter ce type comme le type « pointeur sur un type quelconque (ou non connu) ».
    Ce type de pointeur joue d'une certaine manière le rôle de pointeur générique. Une fonction telle que malloc alloue dynamiquement de la mémoire sans préjuger de l'usage qui en sera fait et de la structure précise de l'espace alloué. Elle renvoie simplement l'adresse d'une zone d'une certaine taille, la définition de la structure de cet espace est laissée à l'application : il n'est même plus nécessaire de faire un cast explicite (le type « pointeur sur void » qui correspond à « void * » est compatible avec tous les types pointeurs).
    Le type de ce qui est pointé n'étant pas connu, il n'est pas possible d'en connaitre la taille et par suite il n'est pas possible de faire avec les pointeurs sur void le même type d'arithmétique qu'avec les autres pointeurs, ni de les déréférencer (c'est-à-dire accéder à la zone pointée avec l'opérateur *).
    Remarque : certaines implémentations du langage C, comme gcc, assimilent un pointeur sur void à un pointeur sur char. Cela autorise l'arithmétique sur ces pointeurs. Mais l'utilisation de cette possibilité limite la portabilité des applications.

while

La forme générale de la boucle est :

while (expression)
instruction

où instruction est une instruction simple ou un bloc.

Il s'agit d'une structure de contrôle permettant d'itérer une instruction (élémentaire ou bloc) tant qu'une certaine condition est vraie.

L'expression contrôlant la boucle est évaluée à l'entrée de la boucle et donc l'instruction n'est éventuellement pas exécutée.

Copyright © <2011>, <Lycée de Taaone/Escot>

Created with the Freeware Edition of HelpNDoc: Easily create CHM Help documents


Vu dans ... http://www.mysti2d.net/polynesie/SIN/08/LangageC/files/Bibliothquestandard.html

Bibliothèque standard

Dernière mise à jour : 2 janvier 2011

La première colonne correspond au type d’usage que l’on fait de l’entité dont le nom figure en deuxième colonne : T correspond à type, C à constante, V à valeur, F à fonction et M à macro. Je parle là d’usage : les constantes sont en vérité définies comme des macros, et certaines fonctions peuvent l’être également. La distinction principale entre une fonction et une « macro » telle que j’emploie le terme ici est qu’il faut faire attention à ne pas inclure d’effet de bord dans les expressions passées en arguments à une macro, celle-ci pouvant les évaluer plusieurs fois, ce qui serait inattendu et incontrôlable, donc catastrophique.

Définitions usuelles (<stddef.h>)

T | ptrdiff_t | différence (en octets) de deux pointeurs
T | size_t | taille d’objets contigus en mémoire
T | wchar_t | caractère étendu (souvent, Unicode)
C | NULL | constante de pointeur nul
M | offsetof | position (en octets) d’un membre dans une structure

Capacité des types entiers (<limits.h>)

C | CHAR_BIT | nombre de bits dans un octet
C | SCHAR_MIN | valeur minimale d’un signed char (-127 ou moins)
C | SCHAR_MAX | valeur maximale d’un signed char (127 ou plus)
C | UCHAR_MAX | valeur maximale d’un unsigned char (255 ou plus)
C | CHAR_MIN | valeur minimale d’un char ; vaut SCHAR_MIN ou 0
C | CHAR_MAX | valeur maximale d’un char ; vaut SCHAR_MAX ou UCHAR_MAX
C | SHRT_MIN | valeur minimale d’un short (-32767 ou moins)
C | SHRT_MAX | valeur maximale d’un short (32767 ou plus)
C | USHRT_MAX | valeur maximale d’un unsigned short (65535 ou plus)
C | INT_MIN | valeur minimale d’un int (-32767 ou moins)
C | INT_MAX | valeur maximale d’un int (32767 ou plus)
C | UINT_MAX | valeur maximale d’un unsigned int (65535 ou plus)
C | LONG_MIN | valeur minimale d’un long (-2147483647 ou moins)
C | LONG_MAX | valeur maximale d’un long (2147483647 ou plus)
C | ULONG_MAX | valeur maximale d’un unsigned long (4294967295 ou plus)
C | MB_LEN_MAX | nombre maximal d’octets par caractère polyoctet

Propriétés des types flottants (<float.h>)

C | FLT_RADIX | base de l’exposant (2 ou plus)
C | FLT_MANT_DIG | nombre de chiffres significatifs en base FLT_RADIX d’un float
C | FLT_MIN_EXP | exposant minimal en base FLT_RADIX d’un float
C | FLT_MAX_EXP | exposant maximal en base FLT_RADIX d’un float
C | FLT_DIG | nombre de chiffres significatifs en base 10 d’un float (6 ou plus)
C | FLT_MIN_10_EXP | exposant minimal en base 10 d’un float (-37 ou moins)
C | FLT_MAX_10_EXP | exposant minimal en base 10 d’un float (37 ou moins)
C | FLT_EPSILON | différence entre 1 et le float supérieur le plus proche (1e-5 ou moins)
C | FLT_MIN | valeur absolue minimale d’un float (1e-37 ou moins)
C | FLT_MAX | valeur absolue maximale d’un float (1e37 ou plus)
C | DBL_MANT_DIG | nombre de chiffres significatifs en base FLT_RADIX d’un double
C | DBL_MIN_EXP | exposant minimal en base FLT_RADIX d’un double
C | DBL_MAX_EXP | exposant maximal en base FLT_RADIX d’un double
C | DBL_DIG | nombre de chiffres significatifs en base 10 d’un double (10 ou plus)
C | DBL_MIN_10_EXP | exposant minimal en base 10 d’un double (-37 ou moins)
C | DBL_MAX_10_EXP | exposant minimal en base 10 d’un double (37 ou moins)
C | DBL_EPSILON | différence entre 1 et le double supérieur le plus proche (1e-5 ou moins)
C | DBL_MIN | valeur absolue minimale d’un double (1e-37 ou moins)
C | DBL_MAX | valeur absolue maximale d’un double (1e37 ou plus)
C | LDBL_MANT_DIG | nombre de chiffres significatifs en base FLT_RADIX d’un long double
C | LDBL_MIN_EXP | exposant minimal en base FLT_RADIX d’un long double
C | LDBL_MAX_EXP | exposant maximal en base FLT_RADIX d’un long double
C | LDBL_DIG | nombre de chiffres significatifs en base 10 d’un long double (10 ou plus)
C | LDBL_MIN_10_EXP | exposant minimal en base 10 d’un long double (-37 ou moins)
C | LDBL_MAX_10_EXP | exposant minimal en base 10 d’un long double (37 ou moins)
C | LDBL_EPSILON | différence entre 1 et le long double supérieur le plus proche (1e-9 ou moins)
C | LDBL_MIN | valeur absolue minimale d’un long double (1e-37 ou moins)
C | LDBL_MAX | valeur absolue maximale d’un long double (1e37 ou plus)

Opérations sur les chaînes de caractères (<string.h>)

Opérations sur les zones de mémoire

F | memset | remplit une zone de mémoire par répétition d’un octet
F | memcpy | copie un zone de mémoire dans une zone disjointe
F | memmove | copie un zone de mémoire dans une autre
F | memcmp | compare une zone de mémoire à une autre
F | memchr | première occurrence d’un octet dans une zone

Opérations sur les chaînes

F | strlen | longueur d’une chaîne
F | strcpy | copie une chaîne dans une zone de mémoire
F | strncpy | copie une chaîne limitée en longueur dans une zone de mémoire
F | strcat | concatène une chaîne à la suite d’une autre
F | strncat | concatène une chaîne limitée en longueur à la suite d’une autre
F | strcmp | compare deux chaînes
F | strncmp | compare deux chaînes limitées en longueur
F | strcoll | compare deux chaînes en tenant compte des paramètres régionaux
F | strxfrm | met une chaîne sous une forme « normale »
F | strchr | première occurrence d’un caractère dans une chaîne
F | strrchr | dernière occurrence d’un caractère dans une chaîne
F | strpbrk | première occurrence d’un caractère d’un ensemble de caractères donné
F | strspn | longueur du préfixe maximal constitué de caractères d’un ensemble de caractères donné
F | strcspn | longueur du préfixe maximal sans caractère d’un ensemble de caractères donné
F | strstr | première occurrence d’une chaîne dans une autre
F | strtok | découpage d’une chaîne à chaque caractères d’un ensemble donné
F | strerror | chaîne décrivant un code d’erreur

Divers

T | size_t | voyez stddef.h
C | NULL | voyez stddef.h

Classification des caractères (<ctype.h>)

M | isprint | teste si un caractère a un glyphe
M | isgraph | teste si un caractère a un glyphe, non blanc
M | isalnum | teste si un caractère est une lettre ou un chiffre
M | isalpha | teste si un caractère est une lettre
M | isupper | teste si un caractère est une lettre majuscule
M | islower | teste si un caractère est une lettre minuscule
M | isdigit | teste si un caractère est un chiffre
M | isxdigit | teste si un caractère est un chiffre hexadécimal
M | ispunct | teste si un caractère est un signe de ponctuation
M | isspace | teste si un caractère est une sorte d’espace
M | iscntrl | teste si un caractère est un caractère de contrôle
M | tolower | convertit un caractère en minuscule
M | toupper | convertit un caractère en majuscule

Fonctions mathématiques (<math.h>)

Sous les systèmes de type Unix, il faut lier le programme avec la bibliothèque -lm.

F | fabs | valeur absolue
F | fmod | reste
F | ceil | plafond
F | floor | plancher
F | modf | parties entières et fractionnaires
F | cos | cosinus
F | sin | sinus
F | tan | tangente
F | acos | arc cosinus
F | asin | arc sinus
F | atan | arc tangente
F | atan2 | arc tangente de deux variables avec ajustement du cadrant
F | exp | exponentielle
F | cosh | cosinus hyperbolique
F | sinh | sinus hyperbolique
F | tanh | tangente hyperbolique
F | log | logarithme népérien
F | log10 | logarithme en base 10
F | pow | puissance
F | sqrt | racine carrée
F | frexp | décomposition en fraction normalisée en base 2
F | ldexp | produit par une puissance de 2
C | HUGE_VAL | valeur représentant un dépassement de capacité

Entrées, sorties et manipulations de fichiers (<stdio.h>)

Opérations sur les fichiers

C | FILENAME_MAX | taille recommandée d’un tableau contenant un nom de fichier
C | L_tmpnam | taille requise d’un tableau pour un nom de fichier temporaire
C | TMP_MAX | minimum garanti de noms de fichier temporaires uniques
F | remove | supprime un fichier
F | rename | renomme (déplace) un fichier
F | tmpnam | génère un nom de fichier temporaire

Opérations sur les flux

T | FILE | flux d’entrée ou de sortie
T | fpos_t | position dans un fichier ; pour fgetpos et fsetpos
C | FOPEN_MAX | minimum garanti de fichiers pouvant être ouverts simultanément
C | EOF | indicateur de fin d’erreur ou de fin de fichier
V | stdin | flux entran standard (souvent, la console)
V | stdout | flux sortant standard (souvent, la console)
V | stderr | flux sortant pour les erreurs (souvent, la console)
F | tmpfile | ouvre un flux temporaire
F | fopen | ouvre un fichier
F | fclose | ferme un flux
F | freopen | rouvre un fichier
F | feof | différencie une fin de fichier d’une erreur
F | ferror | différencie une erreur d’une fin de fichier
F | clearerr | suppression de l’indicateur d’erreur sur un flux
F | ftell | position actuelle du curseur d’un flux
F | fseek | déplace le curseur dans un flux
F | rewind | déplace le curseur d’un flux au début
C | SEEK_SET | point de référence pour fseek : début du flux
C | SEEK_CUR | point de référence pour fseek : position actuelle
C | SEEK_END | point de référence pour fseek : fin du flux
F | fgetpos | position étendue du curseur (fpos_t)
F | fsetpos | déplace le curseur à une position étendue (fpos_t)
F | setbuf | active l’emploi d’un tampon complet pour un flux
F | setvbuf | décide de l’emploi d’un tampon pour un flux
C | _IOFBF | mode pour setvbuf : emploi d’un tampon complet
C | _IOLBF | mode pour setvbuf : emploi d’un tampon pour une ligne
C | _IONBF | mode pour setvbuf : emploi d’aucun tampon
C | BUFSIZ | taille par défaut du tampon
F | fflush | force la synchronisation d’un flux sortant avec son support

Opérations d’entrée et de sortie

F | fgetc | lit un caractère
M | getc | version macro de fgetc ; lit un caractère
M | getchar | lit un caractère sur stdin
F | fputc | écrit un caractère
M | putc | version macro de fputc; écrit un caractère
M | putchar | écrit un caractère sur stdout
F | ungetc | ajoute un caractère à lire au début d’un flux entrant
F | fgets | lit une ligne
F | fputs | écrit une chaîne de caractères
F | puts | écrit une chaîne de caractères et retour à la ligne sur stdout
F | fread | lit par blocs
F | fwrite | écrit par blocs
F | fscanf | lit selon un format
F | scanf | lit selon un format sur stdin
F | fprintf | écrit selon un format
F | printf | écrit selon un format sur stdout
F | vfprintf | écrit selon un format avec une va_list
F | vprintf | écrit selon un format avec une va_list sur stdin
F | sscanf | lit selon un format depuis une chaîne
F | sprintf | écrit selon un format, dans une chaîne
F | vsprintf | écrit selon un format, dans une chaîne, avec une va_list
F | perror | écrit un message d’erreur associé à la valeur d’errno

Divers

T | size_t | voyez stddef.h
C | NULL | voyez stddef.h
F | gets | dangereuse

gets n’aura pas plus de description. Si vous aviez une vraie raison vous poussant à l’utiliser (et je n’en vois aucune), vous n’auriez pas besoin de moi pour vous la rappeler.

Utilitaires généraux (<stdlib.h>)

Interaction avec l’environnement

F | abort | abandonne (termine brutalement) le programme
F | atexit | enregistre une fonction à exécuter en fin de programme
F | exit | termine l’exécution du programme
C | EXIT_FAILURE | code pour exit : échec
C | EXIT_SUCCESS | code pour exit : réussite
F | getenv | récupère une variable d’environnement par son nom
F | system | exécute une commande au shell

Gestion de la mémoire

F | malloc | alloue un bloc de mémoire
F | calloc | alloue un tableau dont les bits sont initialisés à 0
F | free | libère de la mémoire précédemment allouée
F | realloc | change la taille d’un bloc de mémoire

Utilitaires

T | div_t | type de retour de div
T | ldiv_t | type de retour de ldiv
F | abs | valeur absolue d’un int
F | labs | valeur absolue d’un long
F | div | division euclidienne d’int
F | ldiv | division euclidienne de long
F | strtol | convertit un nombre sous forme textuelle en long
F | strtoul | convertit un nombre sous forme textuelle en unsigned long
F | strtod | convertit un nombre sous forme textuelle en double
F | atoi | convertit un nombre sous forme textuelle en int
F | atol | convertit un nombre sous forme textuelle en long
F | atof | convertit un nombre sous forme textuelle en float
F | qsort | trie un tableau
F | bsearch | recherche dans un tableau trié

Chaînes de caractères polyoctets

V | MB_CUR_MAX | nombre maximal d’octets par caractère polyoctet avec les paramètres régionaux en vigueur
F | mblen | longueur d’un caractère polyoctet
F | mbtowc | convertit un caractère polyoctet en caractère étendu
F | wctomb | convertit un caractère étendu en caractère polyoctet
F | mbstowcs | convertit une chaîne de caractères polyoctets en chaîne de caractères étendus
F | wcstombs | convertit une chaîne de caractères étendus en chaîne de caractères polyoctets

Nombres aléatoires

C | RAND_MAX | nombre maximal pouvant être généré par rand
F | srand | initialise le générateur aléatoire
F | rand | génère un entier aléatoire

Divers

T | size_t | voyez stddef.h
T | wchar_t | voyez stddef.h
C | NULL | voyez stddef.h

Dates et heures (<time.h>)

T | time_t | type arithmétique représentant le temps écoulé depuis l’Époque
T | struct tm | date et heure sous forme décomposée
T | clock_t | nombre de battements renvoyés par clock
C | CLOCKS_PER_SEC | nombre de battements renvoyés par clock par seconde
F | time | instant présent
F | difftime | nombre de secondes entre deux time_t
F | mktime | crée un time_t à partir d’un struct tm
F | gmtime | décompose un time_t en un struct tm selon l’heure <acronyme valeur="">GMT</acronyme>
F | localtime | décompose un time_t en un struct tm selon l’heure locale
F | strftime | écrit un struct tm selon un format, dans une chaîne
F | ctime | convertit un time_t en représentation textuelle
F | asctime | convertit un struct tm en représentation textuelle
F | clock | compteur temporel usant d’une unité arbitraire
T | size_t | voyez stddef.h
C | NULL | voyez stddef.h

Diagnostic (<assert.h>)

M | NDEBUG | supprime les appels à assert si défini
M | assert | abandon conditionnel du programme avec message d’erreur

Gestion des erreurs du système (<errno.h>)

V | errno | en cas d’erreur, expression la décrivant
C | EDOM | valeur possible d’errno : erreur mathématique de domaine
C | ERANGE | valeur possible d’errno : erreur mathématique d’intervalle

Gestion des signaux (<signal.h>)

T | sig_atomic_t | type entier muni d’opérations atomiques
F | raise | émet un signal
F | signal | enregistre une action à effectuer à la réception d’un signal
C | SIGABRT | signal d’abandon du programme
C | SIGFPE | signal d’erreur de calcul flottant
C | SIGILL | signal d’instruction illégale
C | SIGINT | signal interactif
C | SIGSEGV | signal d’erreur d’accès à la mémoire
C | SIGTERM | signal de terminaison extérieure du programme
C | SIG_DFL | action pour signal : action par défaut
C | SIG_IGN | action pour signal : ignore un signal reçu
C | SIG_ERR | valeur indicant une erreur

Considérations régionales (<locale.h>)

T | struct lconv | paramètres régionaux
C | LC_ALL | catégorie pour setlocale : toute catégorie
C | LC_COLLATE | catégorie pour setlocale : ordre des caractères
C | LC_CTYPE | catégorie pour setlocale : classification des caractères
C | LC_MONETARY | catégorie pour setlocale : système monétaire
C | LC_NUMERIC | catégorie pour setlocale : nombres
C | LC_TIME | catégorie pour setlocale : dates et heures
F | setlocale | change ou examine la locale en vigueur
F | localeconv | informations sur l’écriture des nombres
C | NULL | voyez stddef.h

Gestion des listes d’arguments variables (<stdarg.h>)

T | va_list | liste d’arguments variable
M | va_start | début d’itération sur une liste d’arguments variable
M | va_arg | prochain argument dans une liste d’arguments variable
M | va_end | fin d’itération sur une liste d’arguments variable

Sauts inter-procéduraux (<setjmp.h>)

T | jmp_buf | informations contextuelles pour les sauts
F | setjmp | sous certaines réserves, sauvegarde le contexte actuel
F | longjmp | téléporte vers un contexte sauvegardé

Édité le 20/12/2008 à 21:14:24 par rz0

Copyright © <2011>, <Lycée de Taaone/Escot>

Created with the Freeware Edition of HelpNDoc: Easily create PDF Help documents


Le langage C • Tutoriels • Zeste de Savoir

Vu dans ... https://zestedesavoir.com/tutoriels/755/le-langage-c-1

Le langage C

Les bases du langage C

  1. Introduction à la programmation
    1. La programmation, qu’est-ce que c’est ?
    2. Le langage C
    3. Notre cible
  2. Les outils nécessaires
    1. Windows
    2. GNU/Linux et *BSD
    3. Mac OS X
    4. Première rencontre
    5. Les commentaires
  3. Les variables
    1. Qu’est-ce qu’une variable ?
    2. Déclarer une variable
    3. Initialiser une variable
    4. Affecter une valeur à une variable
    5. Les représentations octales et hexadécimales
  4. Manipulations basiques des entrées/sorties
    1. Les sorties
    2. Interagir avec l'utilisateur
  5. Les opérations mathématiques
    1. Les opérations mathématiques de base
    2. Les expressions
    3. Sucre syntaxique
    4. Exercices
  6. Tests et conditions
    1. Les booléens
    2. Les opérateurs de comparaison
    3. Les opérateurs logiques
    4. Priorité des opérations
  7. Les sélections
    1. La structure if
    2. L'instruction switch
    3. L'opérateur conditionnel
  8. TP : déterminer le jour de la semaine
    1. Objectif
    2. Première étape
    3. Correction
    4. Deuxième étape
    5. Correction
    6. Troisième et dernière étape
    7. Correction
  9. Les boucles
    1. La boucle while
    2. La boucle do-while
    3. La boucle for
    4. Imbrications
    5. Boucles infinies
    6. Exercices
  10. Les sauts
    1. L'instruction break
    2. L’instruction continue
    3. Boucles imbriquées
    4. L'instruction goto
  11. Les fonctions
    1. Qu'est-ce qu'une fonction ?
    2. Définir et utiliser une fonction
    3. Les prototypes
    4. Variables globales et classes de stockage
    5. Exercices
  12. TP : une calculatrice basique
    1. Objectif
    2. Préparation
    3. Correction
  13. Découper son projet
    1. Portée et masquage
    2. Diviser pour mieux régner
    3. Les fichiers d'en-têtes
  14. La gestion d'erreurs (1)
    1. Détection d'erreurs
    2. Prévenir l'utilisateur
    3. Un exemple d'utilisation des valeurs de retour

Agrégats, mémoire et fichiers

  1. Les pointeurs
    1. Présentation
    2. Déclaration et initialisation
    3. Utilisation
    4. Pointeurs génériques et affichage
    5. Exercice
  2. Les structures
    1. Définition, initialisation et utilisation
    2. Structures et pointeurs
    3. Portée et déclarations
    4. Les structures littérales
    5. Un peu de mémoire
  3. Les tableaux
    1. Les tableaux simples (à une dimension)
    2. La vérité sur les tableaux
    3. Les tableaux multidimensionnels
    4. Les tableaux littéraux
    5. Exercices
  4. Les chaînes de caractères
    1. Qu'est-ce qu'une chaîne de caractères ?
    2. Définition, initialisation et utilisation
    3. Afficher et récupérer une chaîne de caractères
    4. Lire et écrire depuis et dans une chaîne de caractères
    5. Les classes de caractères
    6. Exercices
  5. TP : l'en-tête <string.h>
    1. Préparation
    2. Correction
    3. Pour aller plus loin : strtok
  6. L'allocation dynamique
    1. La notion d'objet
    2. Malloc et consoeurs
    3. Les tableaux multidimensionnels
    4. Les tableaux de longueur variable
  7. Les fichiers (1)
    1. Les fichiers
    2. Les flux : un peu de théorie
    3. Ouverture et fermeture d'un flux
    4. Écriture vers un flux de texte
    5. Lecture depuis un flux de texte
    6. Écriture vers un flux binaire
    7. Lecture depuis un flux binaire
  8. Les fichiers (2)
    1. Détection d'erreurs et fin de fichier
    2. Position au sein d'un flux
    3. La temporisation
    4. Flux ouverts en lecture et écriture
  9. Le préprocesseur
    1. Les inclusions
    2. Les macroconstantes
    3. Les macrofonctions
    4. Les directives conditionnelles
  10. TP : un Puissance 4
    1. Première étape : le jeu
    2. Correction
    3. Deuxième étape : une petite IA
    4. Correction
    5. Troisième et dernière étape : un système de sauvegarde/restauration
    6. Correction
  11. La gestion d'erreurs (2)
    1. Gestion de ressources
    2. Fin d'un programme
    3. Les assertions
    4. Les fonctions strerror et perror

Notions avancées

  1. La représentation des types
    1. La représentation des entiers
    2. La représentations des flottants
    3. La représentation des pointeurs
    4. Ordre des multiplets et des bits
    5. Les fonctions memset, memcpy, memmove et memcmp
  2. Les limites des types
    1. Les limites des types
    2. Les dépassements de capacité
    3. Gérer les dépassements entiers
    4. Gérer les dépassements flottants
  3. Manipulation des bits
    1. Les opérateurs de manipulation des bits
    2. Masques et champs de bits
    3. Les drapeaux
    4. Exercices
  4. Internationalisation et localisation
    1. Définitions
    2. La fonction setlocale
    3. La catégorie LC_NUMERIC
    4. La catégorie LC_TIME
  5. La représentation des chaînes de caractères
    1. Les séquences d'échappement
    2. Les tables de correspondances
    3. Des chaînes, des encodages
  6. Les caractères larges
    1. Introduction
    2. Traduction en chaîne de caractères larges et vice versa
    3. L'en-tête <wchar.h>
    4. <wchar.h> : les fonctions de lecture/écriture
    5. <wchar.h> : les fonctions de manipulation des chaînes de caractères
    6. L'en-tête <wctype.h>
  7. Les énumérations
    1. Définition
    2. Utilisation
  8. Les unions
    1. Définition
    2. Utilisation
  9. Les définitions de type
    1. Définition et utilisation
  10. Les pointeurs de fonction
    1. Déclaration et initialisation
    2. Utilisation
    3. Pointeurs de fonction et pointeurs génériques
  11. Les fonctions et macrofonctions à nombre variable d'arguments
    1. Présentation
    2. L'en-tête <stdarg.h>
    3. Méthodes pour déterminer le nombre et le type des arguments
    4. Les macrofonctions à nombre variable d'arguments
  12. Les sélections génériques
    1. Définition et utilisation
  13. T.P. : un allocateur statique de mémoire
    1. Objectif
    2. Première étape : allouer de la mémoire
    3. Correction
    4. Deuxième étape : libérer de la mémoire
    5. Correction
    6. Troisième étape : fragmentation et défragmentation
    7. Correction
bien mieux ainsi !


Introduction à la programmation - Le langage C • Tutoriels • Zeste de Savoir

Introduction à la programmation ABC

La programmation est un sujet qui fascine énormément. Si vous lisez ce cours, c’est que vous avez décidé de franchir le pas et de découvrir de quoi il s’agit. Cependant, avant de commencer à apprendre quoi que ce soit sur le C et la programmation, il est d’abord nécessaire de découvrir en quoi la programmation consiste. En effet, pour le moment, vous ne savez pas réellement ce qu’est la programmation, ce que signifie « programmer » ou encore ce qui caractérise le langage C. Ce chapitre va donc consister en une introduction au monde de la programmation, et plus particulièrement au langage C.

  • La programmation, qu’est-ce que c’est ?
  • Le langage C
  • Notre cible

La programmation, qu’est-ce que c’est ?

Dans cette section, nous nous contenterons d’une présentation succincte qui est suffisante pour vous permettre de poursuivre la lecture de ce cours. Toutefois, si vous souhaitez un propos plus étayé, nous vous conseillons la lecture du cours d’introduction à la programmation présent sur ce site.

La programmation est une branche de l’informatique qui sert à créer des programmes. Tout ce que vous possédez sur votre ordinateur est un programme : votre navigateur Internet (Internet Explorer, Firefox, Opera, etc.), votre système d’exploitation (Windows, GNU/Linux, Mac OS X, etc.) qui est un regroupement de plusieurs programmes appelés logiciels, votre lecteur MP3, votre logiciel de discussion instantanée, vos jeux vidéos, etc.

Les programmes expliqués en long, en large et en travers

Un programme est une séquence d’instructions, d’ordres, donnés à l’ordinateur afin qu’il exécute des actions. Ces instructions sont généralement assez basiques. On trouve ainsi des instructions d’addition, de multiplication, ou d’autres opérations mathématiques de base, qui font que notre ordinateur est une vraie machine à calculer. D’autres instructions plus complexes peuvent exister, comme des opérations permettant de comparer des valeurs, traiter des caractères, etc.

Créer un programme, c’est tout simplement utiliser une suite d’instructions de base qui permettra de faire ce que l’on veut. Tous les programmes sont créés ainsi : votre lecteur MP3 donne des instructions à l’ordinateur pour écouter de la musique, le chat donne des instructions pour discuter avec d’autres gens sur le réseau, le système d’exploitation donne des instructions pour dire à l’ordinateur comment utiliser le matériel, etc.

Notez qu’il n’est pas possible de créer des instructions. Ces dernières sont imprimées dans les circuits de l’ordinateur ce qui fait qu’il ne peut en gérer qu’un nombre précis et qu’il ne vous est donc pas loisible d’en construire de nouvelles (sauf cas particuliers vraiment tordus).

Notre ordinateur contient un composant électronique particulier, spécialement conçu pour exécuter ces instructions : le processeur. Ce qu’il faut retenir, c’est que notre ordinateur contient un circuit, le processeur, qui permet d’effectuer de petits traitements de base qu’on appelle des instructions et qui sont la base de tout ce qu’on trouve sur un ordinateur.

Les instructions sont stockées dans notre ordinateur sous la forme de chiffres binaires (appelés bits en anglais), autrement dit sous forme de zéros ou de uns. Ainsi, nos instructions ne sont rien d’autre que des suites de zéros et de uns conservées dans notre ordinateur et que notre processeur va interpréter comme étant des ordres à exécuter. Ces suites de zéros et de uns sont difficilement compréhensibles pour nous, humains, et parler à l’ordinateur avec des zéros et des uns est très fastidieux et très long. Autant vous dire que créer des programmes de cette façon revient à se tirer une balle dans le pied.

Pour vous donner un exemple, imaginez que vous deviez communiquer avec un étranger alors que vous ne connaissez pas sa langue. Communiquer avec un ordinateur reviendrait à devoir lui donner une suite de zéros et de uns, ce dernier étant incapable de comprendre autre chose. Ce langage s’appelle le langage machine.

Une question doit certainement vous venir à l’esprit : comment communiquer avec notre processeur sans avoir à apprendre sa langue ?

L’idéal serait de parler à notre processeur en français, en anglais, etc, mais disons-le clairement : notre technologie n’est pas suffisamment évoluée et nous avons dû trouver autre chose. La solution retenue a été de créer des langages de programmation plus évolués que le langage machine, plus faciles à apprendre et de fournir le traducteur qui va avec. Il s’agit de langages assez simplifiés, souvent proches des langages naturels et dans lesquels on peut écrire nos programmes beaucoup plus simplement qu’en utilisant le langage machine. Grâce à eux, il est possible d’écrire nos programmes sous forme de texte, sans avoir à se débrouiller avec des suites de zéros et de uns totalement incompréhensibles. Il existe de nombreux langages de programmation et l’un d’entre eux est le C.

Reste que notre processeur ne comprend pas ces langages évolués et n’en connaît qu’un seul : le sien. Aussi, pour utiliser un langage de programmation, il faut disposer d’un traducteur qui fera le lien entre celui-ci et le langage machine du processeur. Ainsi, il ne vous est plus nécessaire de connaître la langue de votre processeur. En informatique, ce traducteur est appelé un compilateur.

Pour illustrer notre propos, voici un code écrit en C (que nous apprendrons à connaître).

int main(void)
{
    return 0;
}

Et le même en langage machine (plus précisément pour un processeur de la famille x86–64).

01010101 01001000 10001001 11100101 01001000 11000111
11000000 00000000 00000000 00000000 00000000 11001001
11000011

Nous y gagnons tout de même au change, non ? :p

Le langage C

Malgré tous ces langages de programmation disponibles nous allons, dans ce tutoriel, nous concentrer sur un seul d’entre eux : le C. Avant de parler des caractéristiques de ce langage et des choix qui nous amènent à l’étudier dans ce cours, faisons un peu d’histoire.

L’histoire du C

Le langage C est né au début des années 1970 dans les laboratoires de la société AT&T aux États-Unis. Son concepteur, Dennis MacAlistair Ritchie, souhaitait améliorer un langage existant, le B, afin de lui adjoindre des nouveautés. En 1973, le C était pratiquement au point et il commença à être distribué l’année suivante. Son succès fut tel auprès des informaticiens qu’en 1989, l’ ANSI, puis en 1990, l’ ISO, décidèrent de le normaliser, c’est-à-dire d’établir des règles internationales et officielles pour ce langage. À l’heure actuelle, il existe trois normes : la norme ANSI C89 ou ISO C90, la norme ISO C99 et la norme ISO C11.

Si vous voulez en savoir plus sur l’histoire du C, lisez donc ce tutoriel.

Pourquoi apprendre le C ?

C’est une très bonne question. :D
Après tout, étant donné qu’il existe énormément de langages différents, il est légitime de se demander pourquoi choisir le C en particulier ? Il y a plusieurs raisons à cela.

  • Sa popularité : le C fait partie des langages de programmation les plus utilisés. Il possède une communauté très importante, de nombreux cours et beaucoup de documentations. Vous aurez donc toujours du monde pour vous aider. De plus, il existe un grand nombre de projets, programmes et bibliothèques développées en C.
  • Son minimalisme : le C est un langage minimaliste pourvu de peu de concepts et d’abstractions ce qui permet, d’une part, d’en faire le tour relativement rapidement et, d’autre part, de profiter d’une grande rapidité d’exécution et d’une faible emprunte mémoire (ce qui est particulièrement appréciable quand la vitesse d’exécution est cruciale et/ou la quantité de mémoire fort limitée).
  • Sa portabilité : un programme développé en C peut fonctionner sur un grand nombre de machines différentes sans avoir à modifier son code.

Ce ne sont que quelques raisons, mais elles sont à notre goût suffisantes pour justifier l’apprentissage de ce langage. Bien entendu, le C comporte aussi sa part de défauts. On peut citer la tolérance aux comportements dangereux qui fait que le C demande de la rigueur pour ne pas tomber dans certains « pièges », un nombre plus restreint de concepts (c’est parfois un désavantage, car on est alors obligé de recoder certains mécanismes qui existent nativement dans d’autres langages), etc. D’ailleurs, si votre but est de développer rapidement des programmes amusants, sachez que le C n’est pas adapté pour cela et que nous vous conseillons, dans ce cas, de vous tourner vers d’autres langages, comme le Python ou le Ruby.

Le C possède aussi une caractéristique qui est à la fois un avantage et un défaut : il s’agit d’un langage dit de « bas niveau ». Cela signifie qu’il permet de programmer en étant « proche de sa machine », c’est-à-dire sans trop vous cacher son fonctionnement interne. Cette propriété est à double tranchant : d’un côté elle rend l’apprentissage plus difficile et augmente le risque d’erreurs ou de comportements dangereux, mais de l’autre elle vous laisse une grande liberté d’action et vous permet d’en apprendre plus sur le fonctionnement de votre machine. Cette notion de « bas niveau » est d’ailleurs à opposer aux langages dit de « haut niveau » qui permettent de programmer en faisant abstraction d’un certain nombre de choses. Le développement est rendu plus facile et plus rapide, mais en contrepartie, beaucoup de mécanismes internes sont cachés et ne sont pas accessibles au programmeur. Ces notions de haut et de bas niveau sont néanmoins à nuancer, car elles dépendent du langage utilisé et du point de vue du programmeur (par exemple, par rapport au langage machine, le C est un langage de haut niveau).

Une petite note pour terminer : peut-être avez-vous entendu parler du C++ ? Il s’agit d’un langage de programmation qui a été inventé dans les années 1980 par Bjarne Stroustrup, un collègue de Dennis Ritchie, qui souhaitait rajouter des éléments au C. Bien qu’il fût très proche du C lors de sa création, le C++ est aujourd’hui un langage très différent du C et n’a pour ainsi dire plus de rapport avec lui (si ce n’est une certaine proximité au niveau d’une partie de sa syntaxe). Ceci est encore plus vrai en ce qui concerne la manière de programmer et de raisonner qui sont radicalement différentes.

Ne croyez toutefois pas, comme peut le laisser penser leur nom ou leur date de création, qu’il y a un langage meilleur que l’autre, ils sont simplement différents . Si d’ailleurs votre but est d’apprendre le C++, nous vous encourageons à le faire. En effet, contrairement à ce qui est souvent dit ou lu, il n’y a pas besoin de connaitre le C pour apprendre le C++ . Si tel est votre souhait, sachez qu’il existe un cours C++ sur ce site.

La norme

Comme précisé plus haut, le C est un langage qui a été normalisé à trois reprises. Ces normes servent de référence à tous les programmeurs et les aident chaque fois qu’ils ont un doute ou une question en rapport avec le langage. Bien entendu, elles ne sont pas parfaites et ne répondent pas à toutes les questions, mais elles restent la référence pour tout programmeur.

Ces normes sont également indispensables pour les compilateurs. En effet, le respect de ces normes par les différents compilateurs permet qu’il n’y ait pas de différences d’interprétation d’un même code. Finalement, ces normes sont l’équivalent de nos règles d’orthographe, de grammaire et de conjugaison. Imaginez si chacun écrivait ou conjuguait à sa guise, ce serait un sacré bazar…

Dans ce cours, nous avons décidé de nous reposer sur la norme C11, s’agissant de la dernière en date.

Pour les curieux, voici un lien vers le brouillon de cette norme. Cela signifie qu’il ne s’agit pas de la version définitive et officielle, cependant il est largement suffisant pour notre niveau et, surtout, il est gratuit (la norme officielle coûtant très cher :-° ). Notez que celui-ci est rédigé en anglais.

Notre cible

Avant de commencer à programmer, il nous faut aussi définir ce que nous allons programmer, autrement dit le type de programme que nous allons réaliser. Il existe en effet deux grands types de programmes : les programmes graphiques et les programmes en console.

Les programmes graphiques sont les plus courants et les plus connus puisqu’il n’y a pratiquement qu’eux sous Windows ou Mac OS X par exemple. Vous en connaissez sans doute énormément tel que les lecteurs de musique, les navigateurs Internet, les logiciels de discussions instantanées, les suites bureautiques, les jeux vidéos, etc. Ce sont tous des programmes dit « graphiques » car disposant d’une interface graphique (ou GUI).

L'éditeur d'images Gimp est un programme graphique
L'éditeur d'images Gimp est un programme graphique

Cependant, écrire ce genre de programmes demande beaucoup de connaissances, ce qui nous manque justement pour l’instant. Aussi, nous allons devoir nous rabattre sur le deuxième type de programme : les programmes en console.

Les programmes console sont apparus en même temps que l’écran. Ils étaient très utilisés dans les années 1970/1980 (certains d’entre vous se souviennent peut-être de MS-DOS), mais ont fini par être remplacés par une interface graphique avec la sortie de Mac OS et de Windows. Cependant, ils existent toujours et redeviennent quelque peu populaires chez les personnes utilisant GNU/Linux ou *BSD.

Voici un exemple de programme en console (il s’agit de GNU Chess, un jeu d’échecs performant entièrement en ligne de commande).

White (1) : d4
1. d4
black  KQkq  d3
r n b q k b n r
p p p p p p p p
. . . . . . . .
. . . . . . . .
. . . P . . . .
. . . . . . . .
P P P . P P P P
R N B Q K B N R
Thinking...
white  KQkq
r n b q k b . r
p p p p p p p p
. . . . . n . .
. . . . . . . .
. . . P . . . .
. . . . . . . .
P P P . P P P P
R N B Q K B N R
My move is : Nf6
White (2) :

Ce sera le type de programme que nous allons apprendre à créer. Rassurez-vous, quand vous aurez fini le cours, vous aurez les bases pour apprendre à créer des programmes graphiques. ;)

Comme vous avez pu le constater, la programmation est un monde vaste, très vaste, et assez complexe. Comme il existe une multitude de langages de programmation, il faut bien choisir l’un d’entre eux pour commencer. Dans notre cas, il s’agit du C. Ce langage, et retenez-le bien, est à la fois puissant et complexe. Rappelez-vous bien qu’il vous faudra faire des efforts pour l’apprendre correctement.

Si vous vous sentez prêts, alors rendez-vous dans le chapitre suivant, qui vous montrera les outils utilisés par un programmeur C.


Les outils nécessaires - Le langage C • Tutoriels • Zeste de Savoir

Les outils nécessaires

Windows

Le compilateur

Nous vous proposons d’installer le compilateur GCC à l’aide de POSIX sous Windows.

Rendez-vous sur le site de MSys2 et téléchargez la version correspondant à votre système (32 ou 64 bits ). Exécutez ensuite le programme et suivez la procédure d’installation. Une fois l’installation terminée, vous serez face au terminal de MSys2.

Terminal de MSys2
Terminal de MSys2

Entrez alors la commande pacman -Syu et validez ce qui est proposé. Une fois cette commande exécutée, il devrait vous être demandé de fermer le terminal.

Il est demandé de fermer le terminal
Il est demandé de fermer le terminal

Fermez alors cette fenêtre et ouvrez en une nouvelle. Dans celle-ci, exécutez la commande pacman -Su puis pacman -S gcc et validez à chaque fois les actions proposées. Le compilateur est à présent installé. :)

Configuration

Voyez ce qui est dit pour GNU/Linux et *BSD.

L’éditeur de texte

L’éditeur de texte va nous permettre d’écrire notre code source et de l’enregistrer. L’idéal est d’avoir un éditeur de texte facile à utiliser et pas trop minimaliste. Si jamais vous avez déjà un éditeur de texte et que vous l’appréciez, n’en changez pas, il fera sûrement l’affaire.

Si vous n’avez pas d’idée, nous vous conseillons Notepad++ qui est simple, pratique et efficace. Pour le télécharger, rendez-vous simplement dans la rubrique Téléchargements du menu principal.

Veillez-bien à ce que l’encodage de votre fichier soit UTF-8 (sans BOM) (voyez le menu éponyme à cet effet).

Introduction à la ligne de commande

Référez-vous à l’introduction dédiée à GNU/Linux et *BSD.

Le dossier /home/utilisateur (où « utilisateur » correspond à votre nom d’utilisateur) correspond au dossier C:\msysxx\home\utilisateur (où « xx » est 32 ou 64 suivant la version de MSys2 que vous avez installée).

GNU/Linux et *BSD

Le compilateur

Suivant le système que vous choisissez, vous aurez ou non le choix entre différents compilateurs. Si vous n’avez pas d’idée, nous vous conseillons d’opter pour GCC en installant le paquet éponyme à l’aide de votre gestionnaire de paquet.

Configuration

Ceci dépend de votre interpréteur de commande. Pour savoir quel est celui dont vous disposez, ouvrez un terminal (le plus souvent, vous pouvez y accéder via la catégorie Accessoires de votre menu principal) et entrez la commande suivante.

echo $SHELL

bash

Exécutez la commande suivante depuis votre dossier personnel (vous y êtes par défaut au lancement de l’invite de commande).

echo "alias zcc='gcc -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin'" >> .bashrc

csh ou tcsh

Exécutez la commande suivante depuis votre dossier personnel (vous y êtes par défaut au lancement de l’invite de commande).

echo "alias zcc 'gcc -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin'" >> .cshrc # (ou .tcshrc)

ksh, zsh ou sh

Exécutez les commandes suivantes depuis votre dossier personnel (vous y êtes par défaut au lancement de l’invite de commande).

echo "alias zcc='gcc -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin'" >> .kshrc # (ou .zshrc ou .shrc)
echo "export ENV=$HOME/.kshrc" >> .profile # (ou .zshrc ou .shrc)

L’éditeur de texte

Ce serait un euphémisme de dire que vous avez l’embarras du choix. Il existe une pléthore d’éditeurs de texte fonctionnant en ligne de commande ou avec une interface graphique, voire les deux.

Pour n’en citer que quelques-uns, en ligne de commande vous trouverez par exemple : Vim et Emacs (les deux monstres de l’édition), Nano ou Joe. Côté graphique, vous avez entre autres : Gedit, Mousepad et Kate.

Introduction à la ligne de commande

La ligne de commande, derrière son aspect rustre et archaïque, n’est en fait qu’une autre manière de réaliser des tâches sur un ordinateur. La différence majeure avec une interface graphique étant que les instructions sont données non pas à l’aide de boutons et de cliques de souris, mais exclusivement à l’aide de texte. Ainsi, pour réaliser une tâche donnée, il sera nécessaire d’invoquer un programme (on parle souvent de commandes) en tapant son nom.

La première chose que vous devez garder à l’esprit, c’est le dossier dans lequel vous vous situez. Suivant le terminal que vous employez, celui-ci est parfois indiqué en début de ligne et terminé par le symbole $ ou % . Ce dossier est celui dans lequel les actions (par exemple la création d’un répertoire) que vous demanderez seront exécutées. Normalement, par défaut, vous devriez vous situez dans le répertoire : /home/utilisateur (où utilisateur correspond à votre nom d’utilisateur). Ceci étant posé, voyons quelques commandes basiques.

La commande pwd (pour print working directory ) vous permet de connaître le répertoire dans lequel vous êtes.

$ pwd
/home/utilisateur

La commande mkdir (pour make directory ) vous permet de créer un nouveau dossier. Pour ce faire, tapez mkdir suivi d’un espace et du nom du nouveau répertoire. Par exemple, vous pouvez créer un dossier programmation comme suit :

$ mkdir programmation

La commande ls (pour list ) vous permet de lister le contenu d’un dossier. Vous pouvez ainsi vérifier qu’un nouveau répertoire a bien été créé.

$ ls
programmation

Le résultat ne sera pas forcément le même que ci-dessus, cela dépend du contenu de votre dossier. L’essentiel est que vous retrouviez bien le dossier que vous venez de créer.

Enfin, la commande cd (pour change directory ) vous permet de vous déplacer d’un dossier à l’autre. Pour ce faire, spécifiez simplement le nom du dossier de destination.

$ cd programmation
$ pwd
/home/utilisateur/programmation

Le dossier spécial .. représente le répertoire parent. Il vous permet donc de revenir en arrière dans la hiérarchie des dossiers. Le dossier spécial . représente quant à lui le dossier courant.

Voilà, avec ceci, vous êtes fin prêt pour compiler votre premier programme. Vous pouvez vous rendre à la deuxième partie de ce chapitre.

Mac OS X

Le compilateur

Allez dans le dossier /Applications/Utilitaires et lancez l’application terminal.app . Une fois ceci fait, entrez la commande suivante :

xcode-select --install

et cliquez sur Installer dans la fenêtre qui apparaît. Si vous rencontrez le message d’erreur ci-dessous, cela signifie que vous disposez déjà des logiciels requis.

Impossible d’installer ce logiciel car il n’est pas disponible actuellement depuis le serveur de mise à jour de logiciels.

Si vous ne disposez pas de la commande indiquée, alors rendez-vous sur le site de développeur d’Apple : Apple developer connection . Il faudra ensuite vous rendre sur le Mac Dev Center puis, dans Additional download , cliquez sur View all downloads . Quand vous aurez la liste, il vous suffit de chercher la version 3 de Xcode (pour Leopard et Snow Leopard ) ou 2 pour les versions antérieures ( Tiger ). Vous pouvez aussi utiliser votre CD d’installation pour installer Xcode (sauf pour Lion ).

Configuration

Voyez ce qui est dit pour GNU/Linux et *BSD.

L’éditeur de texte

Comme pour GNU/Linux et *BSD, vous trouverez un bon nombres d’éditeurs de texte. Si toutefois vous êtes perdu, nous vous conseillons TextWrangler ou Smultron.

Introduction à la ligne de commande

Référez-vous à l’introduction dédiée à GNU/Linux et *BSD.

Première rencontre

Bien, il est à présent temps d’écrire et de compiler notre premier programme ! Pour ce faire, ouvrez votre éditeur de texte et entrez les lignes suivantes.

int main(void)
{
    return 0;
}

Ensuite, enregistrez ce fichier dans un dossier de votre choix et nommez-le « main.c ».

Rappelez-vous, sous Windows, le dossier /home/utilisateur (où « utilisateur » correspond à votre nom d’utilisateur) correspond au dossier C:\msysxx\home\utilisateur (où « xx » est 32 ou 64 suivant la version de MSys2 que vous avez installée).

Une fois ceci fait, rendez-vous dans le dossier contenant le fichier à l’aide d’un terminal et exécutez la commande ci-dessous.

zcc main.c

Si vous utilisez l’interpréteur de commande sh , ksh ou zsh , la commande zcc ne sera connue de votre invite de commandes qu’une fois que vous aurez ouvert une nouvelle session. En attendant, vous pouvez entrez la commande alias zcc='gcc -Wall -Wextra -pedantic -std=c11 -fno-common -fno-builtin' dans votre terminal pour que cela fonctionne.

Si tout se passe bien, vous devriez obtenir un fichier « a.exe » sous Windows et un fichier « a.out » sinon. Vous pouvez exécutez ce programme en tapant ./a.exe ou ./a.out .

Je viens de le faire, mais il ne se passe rien…

Cela tombe bien, c’est exactement ce que fait ce programme : rien. :p
Voyons cela plus en détails.

Ce bout de code est appelé une fonction qui est la brique de base de tout programme écrit en C. Une fonction se compose d’une ou plusieurs instructions chacune terminée par un point-virgule ( ; ) et comprise entre deux accolades ( { et } ). Ici, notre fonction se nomme main . Il s’agit d’une fonction obligatoire dans tout programme C, car il s’agit de la fonction par laquelle l’exécution commence et se termine.

Notre fonction main() ne comporte qu’une seule instruction : return 0; , qui met fin à son exécution et permet d’indiquer à notre système d’exploitation que l’exécution s’est correctement déroulée (une valeur différente de zéro indiquerait une erreur).

Le nom de la fonction est précédé du mot-clé int (pour integer ) qui est un nom de type indiquant que la fonction retourne une valeur entière. À l’intérieur des parenthèses, il y a le mot void qui signifie que la fonction ne reçoit pas de paramètres, nous reviendrons sur tout cela en temps voulu.

Les commentaires

Il est souvent nécessaire de commenter son code source pour décrire des passages un peu moins lisibles ou tout simplement pour offrir quelques compléments d’information au lecteur du code. Nous en utiliserons souvent dans la suite de ce cours pour rendre certains exemples plus parlant.

Un commentaire est ignoré par le compilateur, il disparait et n’est pas présent dans l’exécutable. Il ne sert qu’au programmeur et aux lecteurs du code.

Un commentaire en C est écrit soit en étant précédé de deux barres obliques // , soit en étant placé entre les signes /* et */ . Dans le dernier cas, le commentaire peut alors s’étendre sur plusieurs lignes.

// Ceci est un commentaire.

/* Ceci est un autre commentaire. */

/* Ceci est un commentaire qui
   prend plusieurs lignes. */

Voilà, vous avez enfin fait la connaissance du C à travers du code. Certes, nous n’avons vu qu’un petit code et avons seulement survolé les différents éléments, mais il n’empêche que cela représente certainement beaucoup de nouveautés pour vous. Relisez donc ce chapitre à tête reposée si nécessaire.


Les variables - Le langage C • Tutoriels • Zeste de Savoir

Les variables

Qu’est-ce qu’une variable ?

Pour comprendre ce qu’est une variable et comment manipuler celles-ci, il faut commencer par comprendre comment notre ordinateur fait pour stocker des données. En théorie, un ordinateur est capable de stocker tout type d’information. Mais comment est-il possible de réaliser un tel miracle alors qu’il ne s’agit finalement que d’un amas de circuits électriques ?

Codage des informations

Peut-être avez-vous déjà entendu le proverbe suivant : « si le seul outil que vous avez est un marteau, vous verrez tout problème comme un clou » (Abraham Maslow). Hé bien, l’idée est un peu la même pour un ordinateur : ce dernier ne sachant utiliser que des nombres, il voit toute information comme une suite de nombres.

L’astuce consiste donc à transformer une information en nombre pour que l’ordinateur puisse la traiter, autrement dit la numériser. Différentes techniques sont possibles pour atteindre cet objectif, une des plus simples étant une table de correspondance, par exemple entre un nombre et un caractère.

Caractère | Nombre
A | 1
B | 2
C | 3

Binaire

Cependant, comme si cela ne suffisait pas, un ordinateur ne compte pas comme nous : il compte en base deux.

En base deux ?

La base correspond au nombre de chiffres disponibles pour représenter un nombre. En base 10, nous disposons de dix chiffres : zéro, un, deux, trois, quatre, cinq, six, sept, huit et neuf. En base deux, nous en avons donc… deux : zéro et un. Pour ce qui est de compter, c’est du pareil au même : nous commençons par épuiser les unités : 0, 1 ; puis nous passons aux dizaines : 10, 11 ; puis aux centaines : 100, 101, 110, 111 ; et ainsi de suite. Ci-dessous un petit tableau de correspondance entre la base deux et la base dix.

Base deux | Base dix
0 | 0
1 | 1
10 | 2
11 | 3
100 | 4
101 | 5
110 | 6
111 | 7
1000 | 8
1001 | 9
1010 | 10

Un chiffre binaire (un zéro ou un un) est appelé un bit en anglais. Il s’agit de la contraction de l’expression « binary digit ». Nous l’emploierons assez souvent dans la suite de ce cours par souci d’économie.

Mais pourquoi utiliser la base deux et non la base dix ?

Parce que les données circulent sous forme de courants électriques. Or, la tension de ceux-ci n’étant pas toujours stable, il est difficile de réaliser un système fiable sachant détecter dix valeurs différentes. Par contre, c’est parfaitement possible avec deux valeurs : il y a du courant ou il n’y en a pas.

La mémoire

Nous savons à présent que notre ordinateur ne sait employer que des nombres représentés en base deux.

Mais comment stocker tout ce fatras de nombres ?

Hé bien, les bits sont stockés dans un composant électronique particulier de l’ordinateur : la mémoire. Enfin, nous disons « la mémoire », mais il y en a en fait plusieurs.

Mais pourquoi plusieurs mémoires et pas une seule ?

Le fait est qu’il est actuellement impossible de créer des mémoires qui soient à la fois rapides et capables de contenir beaucoup de données. Nous ne pouvons donc utiliser une seule grosse mémoire capable de stocker toutes les données dont nous avons besoin. Ce problème s’est posé dès les débuts de l’informatique, comme en témoigne cette citation des années 1940, provenant des concepteurs d’un des tout premiers ordinateurs.

Idéalement, nous désirerions une mémoire d’une capacité indéfiniment large telle que n’importe quelle donnée soit immédiatement accessible. Nous sommes forcés de reconnaître la possibilité de la construction d’une hiérarchie de mémoire, chacune ayant une capacité plus importante que la précédente, mais accessible moins rapidement.

Burks, Goldstine, et Von Neumann

Mais les chercheurs et ingénieurs du début de l’informatique ont trouvé une solution : segmenter la mémoire de l’ordinateur en plusieurs sous-mémoires, de taille et de vitesse différentes, utilisées chacune suivant les besoins. Nous aurons donc des mémoires pouvant contenir peu de données et rapides, à côté de mémoires plus importantes et plus lentes.

Nous vous avons dit que l’ordinateur utilisait plusieurs mémoires. Trois d’entre elles méritent à notre sens votre attention :

  • les registres ;
  • la mémoire vive (ou RAM en anglais) ;
  • le disque dur.

Les registres sont des mémoires intégrées dans le processeur, utilisées pour stocker des données temporaires. Elles sont très rapides, mais ne peuvent contenir que des données très simples, comme des nombres.

La mémoire vive est une mémoire un peu plus grosse, mais plus lente que les registres. Elle peut contenir pas mal de données et est généralement utilisée pour stocker les programmes en court d’exécution ainsi que les données qu’ils manipulent.

Ces deux mémoires (les registres et la mémoire vive) ont tout de même un léger défaut : elles perdent leur contenu quand elles ne sont plus alimentées… Autant dire que ce n’est pas le meilleur endroit pour stocker un système d’exploitation ou des fichiers personnels. Ceci est le rôle du disque dur, une mémoire avec une capacité très importante, mais très lente qui a toutefois l’avantage d’assurer la persistance des données.

En C, la mémoire la plus manipulée par le programmeur est la mémoire vive. Aussi, nous allons nous y intéresser d’un peu plus près dans ce qui suit.

Bits , multiplets et octets

Dans la mémoire vive, les bits sont regroupés en « paquets » de quantité fixe : des « cases mémoires », aussi appelées multiplets (ou bytes en anglais). À quelques exceptions près, les mémoires utilisent des multiplets de huit bits , aussi appelés octet. Un octet peut stocker 256 informations différentes (vous pouvez faire le calcul vous-même : combien vaut 11111111 en base deux ? :p ). Pour stocker plus d’informations, il sera nécessaire d’utiliser plusieurs octets.

Adresse mémoire

Néanmoins, il est bien beau de stocker des données en mémoire, encore faut-il pouvoir remettre la main dessus.

Dans cette optique, chaque multiplet de la mémoire vive se voit attribuer un nombre unique, une adresse, qui va permettre de le sélectionner et de l’identifier parmi tous les autres. Imaginez la mémoire vive de l’ordinateur comme une immense armoire, qui contiendrait beaucoup de tiroirs (les cases mémoires) pouvant chacun contenir un multiplet. Chaque tiroir se voit attribuer un numéro pour le reconnaitre parmi tous les autres. Nous pourrions ainsi demander quel est le contenu du tiroir numéro 27. Pour la mémoire, c’est pareil. Chaque case mémoire a un numéro : son adresse.

Adresse | Contenu mémoire
0 | 11101010
1 | 01111111
2 | 00000000
3 | 01010101
4 | 10101010
5 | 00000000

En fait, vous pouvez comparer une adresse à un numéro de téléphone : chacun de vos correspondants a un numéro de téléphone et vous savez que pour appeler telle personne, vous devez composer tel numéro. Les adresses mémoires fonctionnent exactement de la même façon !

Plus généralement, toutes les mémoires disposent d’un mécanisme similaire pour retrouver les données. Aussi, vous entendrez souvent le terme de référence qui désigne un moyen (comme une adresse) permettant de localiser une donnée. Il s’agit simplement d’une notion plus générale.

Les variables

Tout cela est bien sympathique, mais manipuler explicitement des références (des adresses si vous préférez) est un vrai calvaire, de même que de s’évertuer à calculer en base deux. Heureusement pour nous, les langages de programmation (et notamment le C), se chargent d’effectuer les conversions pour nous et remplacent les références par des variables.

Une variable correspondra à une portion de mémoire, appelée objet, à laquelle nous donnerons un nom. Ce nom permettra d’identifier notre variable, tout comme une référence permet d’identifier une portion de mémoire parmi toutes les autres. Nous allons ainsi pouvoir nommer les données que nous manipulons, chacun de ces noms étant remplacés lors de la compilation par une référence (le plus souvent une adresse).

Déclarer une variable

Entrons maintenant dans le vif du sujet en apprenant à déclarer nos variables. Tout d’abord, sachez qu’une variable est constituée de deux éléments obligatoires :

  • un type ;
  • un identificateur qui est en gros le « nom » de la variable.

Le type d’une variable permet d’indiquer ce qui y sera stocké, par exemple : un caractère, un nombre entier, un nombre réel (ou nombre à virgule flottante, parfois tout simplement abrégé en « flottant »), etc. Pour préciser le type d’une variable, il est nécessaire d’utiliser un mot-clé spécifique (il y en a donc un pour chaque type).

Une fois que nous avons décidé du nom et du type de notre variable, nous pouvons la créer (on dit aussi la déclarer) comme suit.

type identificateur;

En clair, il suffit de placer un mot-clé indiquant le type de la variable et de placer le nom qu’on lui a choisi immédiatement après.

Faites bien attention au point-virgule à la fin !

Les types

Comme dit précédemment, un type permet d’indiquer au compilateur quel genre de données nous souhaitons stocker. Ce type va permettre de préciser :

  • toutes les valeurs que peut prendre la variable ;
  • les opérations qu’il est possible d’effectuer avec (il n’est par exemple pas possible de réaliser une division entière avec un nombre à virgule flottante, nous y reviendrons).

Définir le type d’une variable permet donc de préciser son contenu potentiel et ce que nous pouvons faire avec. Le langage C fournit neuf types de base.

Type | Sert à stocker
_Bool | un entier
char | un caractère ou un entier
short int | un entier
int | un entier
long int | un entier
long long int | un entier
float | un réel
double | un réel
long double | un réel

Les types short int , int , long int et long long int servent tous à stocker des nombres entiers qui peuvent prendre des valeurs positives, négatives, ou nulles. On dit qu’il s’agit de types signés (car ils peuvent comporter un signe). Pour chacun de ces quatre types, il existe un type équivalent dit non signé. Un type entier non signé est un type entier qui n’accepte que des valeurs positives ou nulles : il ne peut pas stocker de valeurs négatives. Pour déclarer des variables d’un type non signé, il vous suffit de faire précéder le nom du type entier du mot-clé unsigned .

Le type _Bool 3 est un type entier non signé un peu particulier : il permet de stocker soit 0, soit 1, on parle d’un booléen (nous verrons de quoi il s’agit un peu plus tard). À ce titre, _Bool est le type entier non signé avec la plus faible capacité.

Le type char peut lui aussi servir à stocker des nombres. Il sert surtout au stockage de caractères, mais ces derniers étant stockés dans l’ordinateur sous forme de nombres, il est possible de stocker des nombres dans un char . Le seul problème, c’est que ce type peut être signé ou non signé de base suivant les compilateurs. Pour éviter les ennuis, spécifiez ce que vous souhaitez lors de la déclaration : non signé ( unsigned char ) ou signé ( signed char ).

Les types float , double et long double permettent quant à eux de stocker des nombres réels. 4

En cas de manque d’information concernant le type lors d’une déclaration, c’est le type int qui sera utilisé. Ainsi, short , long et long long sont respectivement des raccourcis pour short int , long int et long long int . De même, le mot-clé unsigned seul signifie unsigned int .

Capacité d’un type

Tous les types stockant des nombres ont des bornes, c’est-à-dire une limite aux valeurs qu’ils peuvent stocker. En effet, le nombre de multiplets occupés par une variable est limité suivant son type. En conséquence, il n’est pas possible de mettre tous les nombres possibles dans une variable de type int , float , ou double . Il y aura toujours une valeur minimale et une valeur maximale. Ces limites sont les suivantes.

Type | Minimum | Maximum
_Bool | 0 | 1
signed char | -127 | 127
unsigned char | 0 | 255
short | -32 767 | 32 767
unsigned short | 0 | 65 535
int | -32 767 | 32 767
unsigned int | 0 | 65 535
long | -2 147 483 647 | 2 147 483 647
unsigned long | 0 | 4 294 967 295
long long | -9 223 372 036 854 775 807 | 9 223 372 036 854 775 807
unsigned long long | 0 | 18 446 744 073 709 551 615
float | -1 × 1037 | 1 × 1037
double | -1 × 1037 | 1 × 1037
long double | -1 × 1037 | 1 × 1037

Si vous regardez bien ce tableau, vous remarquez que certains types ont des bornes identiques. En vérité, les valeurs présentées ci-dessus sont les minimums garantis par la norme 1 et il est fort probable qu’en réalité, vous puissiez stocker des valeurs plus élevées que ceux-ci. Cependant, dans une optique de portabilité, vous devez considérer ces valeurs comme les minimums et les maximums de ces types, peu importe la capacité réelle de ces derniers sur votre machine.

Taille d’un type

Peut-être vous êtes vous demandés pourquoi il existe autant de types différents. La réponse est toute simple : la taille des mémoires était très limitée à l’époque où le langage C a été créé. En effet, le PDP-11 sur lequel le C a été conçu ne possédait que 24Ko de mémoire (pour comparaison, une calculatrice TI-Nspire possède 100Mo de mémoire, soit environ 4000 fois plus). Il fallait donc l’économiser au maximum en choisissant le type le plus petit possible. Cette taille dépend des machines, mais de manière générale, vous pouvez retenir les deux suites d’inégalités suivantes : char ≤ short ≤ int ≤ long ≤ long long et float ≤ double ≤ long double .

Aujourd’hui ce n’est plus un problème, il n’est pas nécessaire de se casser la tête sur quel type choisir (excepté si vous voulez programmer pour de petits appareils où la mémoire est plus petite). En pratique, nous utiliserons surtout char pour les caractères, int , long ou long long pour les entiers et double pour les réels.

Les identificateurs

Maintenant que nous avons vu les types, parlons des identificateurs. Comme dit précédemment, un identificateur est un nom donné à une variable pour la différencier de toutes les autres. Et ce nom, c’est au programmeur de le choisir. Cependant, il y a quelques limitations à ce choix.

  • seuls les 26 lettres de l’alphabet latin (majuscules ou minuscules), le trait de soulignement « _ » ( underscore en anglais) et les chiffres sont acceptés. Pas d’accents, pas de ponctuation ni d’espaces ;
  • un identificateur ne peut pas commencer par un chiffre ;
  • les mots-clés ne peuvent pas servir à identifier une variable ; il s’agit de :

                                auto         if          unsigned
                                break        inline      void
                                case         int         volatile
                                char         long        while
                                const        register    _Alignas
                                continue     restrict    _Alignof
                                default      return      _Atomic
                                do           short       _Bool
                                double       signed      _Complex
                                else         sizeof      _Generic
                                enum         static      _Imaginary
                                extern       struct      _Noreturn
                                float        switch      _Static_assert
                                for          typedef     _Thread_local
                                goto         union
  • deux variables ne peuvent avoir le même identificateur (le même nom). Il y a parfois quelques exceptions, mais cela n’est pas pour tout de suite ;
  • les identificateurs peuvent être aussi longs que l’on désire, toutefois le compilateur ne tiendra compte que des 63 premiers caractères 2.

Voici quelques exemples pour bien comprendre.

Identificateur correct | Identificateur incorrect | Raison
variable | Nom de variable | Espaces interdits
nombre_de_vie | 1nombre_de_vie | Commence par un chiffre
test | test! | Caractère « ! » interdit
un_dernier_pour_la_route1 | continue | Mot-clé réservé par le langage

À noter que le C fait la différence entre les majuscules et les minuscules (on dit qu’il respecte la casse). Ainsi les trois identificateurs suivants sont différents.

variable
Variable
VaRiAbLe

Déclaration

Maintenant que nous connaissons toutes les bases, entrainons-nous à déclarer quelques variables.

int main(void)
{
    _Bool booleen;
    double taille;
    unsigned int age;
    char caractere;
    short petite_valeur;
    return 0;
}

Il est possible de déclarer plusieurs variables de même type sur une même ligne, en séparant leurs noms par une virgule.

int age, taille, nombre;

Ceci permet de regrouper les déclarations suivant les rapports que les variables ont entre elles.

int annee, mois, jour;
int age, taille;
int x, y, z;

  1. ISO/IEC 9899:201x, doc. N1570, § 5.2.4.2, Numerical limits, p.26

    ↩

  2. ISO/IEC 9899:201x, doc. N1570, § 5.2.4, Environmental limits, p.25

    ↩

  3. Notez que le type _Bool a une orthographe différente des autres types (il commence par un trait de soulignement et une majuscule), cela s’explique par le fait qu’il a été introduit après la norme C89.

    ↩

  4. Depuis la norme C99, il existe pour chaque type réel un type complexe correspondant permettant, comme son nom l’indique, de contenir des nombres complexes. Pour ce faire, il est nécessaire d’ajouter le mot-clé _Complex au type souhaité, par exemple double _Complex . Toutefois, l’intérêt étant limité, nous n’en parlerons pas plus dans ce cours.

    ↩

Initialiser une variable

En plus de déclarer une variable, il est possible de l’initialiser, c’est-à-dire de lui attribuer une valeur. La syntaxe est la suivante.

type identificateur = valeur;

Initilisation des types entiers

Pour les types entiers, c’est assez simple, la valeur à attribuer est… un nombre entier.

_Bool booleen = 0;
unsigned char age = 42;
long score = -1;

Initialisation des types réels

Pour les types réels, ceux-ci étant faits pour contenir des nombres à virgule, à l’initialisation, il est nécessaire de placer cette « virgule ». Toutefois, cette dernière est représentée par un point.

double pi = 3.14;

Ceci vient du fait que le C est une invention américaine et que les anglophones utilisent le point à la place de la virgule.

Notez qu’il est important de bien placer ce point, même si vous voulez stocker un nombre rond . Par exemple, vous ne devez pas écrire double a = 5 mais double a = 5. (certains préfèrent double a = 5.0 , cela revient au même).

En notation scientifique

Par ailleurs, il est également possible de représenter un nombre à virgule flottante à l’aide de la notation scientifique, c’est-à-dire sous la forme d’un nombre décimal et d’une puissance de dix. Cela se traduit par un nombre réel en notation simple (comme ci-dessus) suivi de la lettre « e » ou « E » et d’un exposant entier .

double f = 1E-1; /* 1x10^-1 = 0.1 */

Initialisation du type char

Nous l’avons précisé : le type char peut être utilisé pour contenir un entier ou un caractère. Dans le second cas, le caractère utilisé pour l’initialisation doit être entouré de guillemets simples, comme suit.

char a = 'a';

Rendre une variable constante

En plus d’attribuer une valeur à une variable, il est possible de préciser que cette variable ne pourra pas être modifiée par la suite à l’aide du mot-clé const . Ceci peut être utile pour stocker une valeur qui ne changera jamais (comme la constante π π \pi π qui vaut toujours 3,14159265).

const double pi = 3.14159265;

Affecter une valeur à une variable

Nous savons donc déclarer (créer) nos variables et les initialiser (leur donner une valeur à la création). Il ne nous reste plus qu’à voir la dernière manipulation possible : l’affectation. Celle-ci permet de modifier la valeur contenue dans une variable pour la remplacer par une autre valeur.

Il va de soi que cette affectation n’est possible que pour les variables qui ne sont pas déclarées avec le mot-clé const puisque, par définition, de telles variables sont constantes et ne peuvent voir leur contenu modifié.

Pour faire une affectation, il suffit d’opérer ainsi.

identificateur = nouvelle_valeur;

Nous voyons que la syntaxe est similaire à celle d’une déclaration avec initialisation. La seule différence, c’est que nous n’avons pas à préciser le type. Celui-ci est en effet fixé une fois pour toute lors de la déclaration de notre variable. Aussi, il n’est pas nécessaire de le préciser à nouveau lors d’une affectation.

age = 30;
taille = 177.5;
petite_valeur = 2;

Notez qu’il n’y a aucune limite au nombre d’affectations, comme le démontre l’exemple ci-dessous.

petite_valeur = 2;
petite_valeur = 4;
petite_valeur = 8;
petite_valeur = 16;
petite_valeur = 8;
petite_valeur = 4;
petite_valeur = 2;

À chaque affectation, la variable va prendre une nouvelle valeur. Par contre, ne mettez pas le type quand vous voulez changer la valeur, sinon vous aurez le droit à une belle erreur du type « redefinition of 'nom_de_votre_variable' » car vous aurez créé deux variables avec le même identificateur !

Le code suivant est donc incorrect.

int age = 15;
int age = 20;

Si vous exécutez tous ces codes, vous verrez qu’ils n’affichent toujours rien et c’est normal puisque nous n’avons pas demandé à notre ordinateur d’afficher quoique ce soit. Nous apprendrons comment faire au chapitre suivant.

Il n’y a pas de valeur par défaut en C. Aussi, sans initialisation ou affectation, la valeur d’une variable est indéterminée ! Veillez donc à ce que vos variables aient une valeur connue avant de les utiliser !

Les représentations octales et hexadécimales

Pour terminer ce chapitre, nous vous proposons un petit aparté sur deux représentations particulières : la représentation octale et la représentation hexadécimale.

Nous avons déjà vu la représentation binaire au début de ce chapitre, les représentations octale et hexadécimale obéissent au même schéma : au lieu d’utiliser dix chiffres pour représenter un nombre, nous en utilisons respectivement huit ou seize.

Seize chiffres ?! Mais… Je n’en connais que dix moi !

La représentation hexadécimale est un peu déroutante de prime abord, celle-ci ajoute six chiffres (en fait, six lettres) : a, b, c, d, e et f. Pour vous aider, voici un tableau présentant les nombres de 0 à 16 en binaire, octal, décimal et hexadécimal.

Binaire | Octal | Décimal | Hexadécimal
00000 | 0 | 0 | 0
00001 | 1 | 1 | 1
00010 | 2 | 2 | 2
00011 | 3 | 3 | 3
00100 | 4 | 4 | 4
00101 | 5 | 5 | 5
00110 | 6 | 6 | 6
00111 | 7 | 7 | 7
01000 | 10 | 8 | 8
01001 | 11 | 9 | 9
01010 | 12 | 10 | a
01011 | 13 | 11 | b
01100 | 14 | 12 | c
01101 | 15 | 13 | d
01110 | 16 | 14 | e
01111 | 17 | 15 | f
10000 | 20 | 16 | 10

Notez que dix dans n’importe quelle base équivaut à cette base.

Quel est l’intérêt de ces deux bases exactement ?

L’avantage des représentations octale et hexadécimale est qu’il est facilement possible de les convertir en binaire contrairement à la représentation décimale. En effet, un chiffre en base huit ou en base seize peut-être facilement traduit, respectivement, en trois ou quatre bits .

Prenons l’exemple du nombre 35 qui donne 43 en octal et 23 en hexadécimal. Nous pouvons nous focaliser sur chaque chiffre un à un pour obtenir la représentation binaire. Ainsi, du côté de la représentation octale, 4 donne 100 et 3 011 , ce qui nous donne finalement 00100011 . De même, pour la représentation hexadécimale, 2 nous donne 0010 et 3 0011 et nous obtenons 00100011 . Il n’est pas possible de faire pareil en décimal.

En résumé, les représentations octale et hexadécimale permettent de représenter un nombre binaire de manière plus concise et plus lisible.

Constantes octales et hexadécimales

Il vous est possible de préciser la base d’une constante entière en utilisant des préfixes. Ces préfixes sont 0 pour les constantes octales et 0x ou 0X pour les constantes hexadécimales.

long a = 65535; /* En décimal */
int b = 0777; /* En octal */
short c = 0xFF; /* En hexadécimal */

Les lettres utilisées pour la représentation hexadécimale peuvent être aussi bien écrites en minuscule qu’en majuscule.

Il n’est pas possible d’utiliser une constante en base deux ?

Malheureusement, non, le langage C ne permet pas d’utiliser de telles constantes.

Voilà, c’est la fin de ce chapitre. Nous avons vu beaucoup de choses, n’hésitez pas à potasser pour bien assimiler tout ça. Les variables sont vraiment la base de la programmation, aussi il est nécessaire de bien les comprendre. Rendez-vous au prochain chapitre qui sera très intéressant : vous pourrez par exemple demander l’âge de l’utilisateur pour ensuite l’afficher !

En résumé
  1. Les informations sont numérisées (traduites en nombres) afin de pouvoir être traitées par un ordinateur ;
  2. Un ordinateur calcule en base deux ;
  3. Un chiffre binaire est appellé un bit (contraction de l’expression « binary digit ») ;
  4. Les bits sont regroupés en mutiplets (ou byte en anlgais) qui sont le plus souvent composés de huit bits (on parle alors d’octet) ;
  5. Les données sont stockées dans différents types de mémoire, notamment : les registres, la mémoire vive (ou RAM) et les disques durs ;
  6. Chaque multiplet de la RAM est identifié par une adresse ;
  7. Une variable est déclarée à l’aide d’un type et d’un identificateur ;
  8. Le C fourni neuf types de base : _Bool , char , short int , int , long int , long long int , float , double , long double ;
  9. Tous les types ont une capacité limitée avec un minimum garantit par la norme ;
  10. Une variable peut être initialisée en lui fournissant une valeur lors de sa déclaration ;
  11. Il est possible d’affecter une valeur à une variable une fois celle-ci déclarée ;
  12. Sans initialisation ou affectation, la valeur d’une variable est indéterminée ;
  13. Il est possible de représenter des nombres entiers en base 8 (octale) ou en base 16 (hexadécimale).


Manipulations basiques des entrées/sorties - Le langage C • Tutoriels • Zeste de Savoir

Manipulations basiques des entrées/sorties

Les sorties

Intéressons-nous dans un premier temps aux sorties. Afin d’afficher du texte, nous avons besoin d’une fonction.

Une fonction est un morceau de code qui a un but, une fonction particulière et qui peut être appelée à l’aide d’une référence, le plus souvent le nom de cette fonction (comme pour une variable, finalement). En l’occurrence, nous allons utiliser une fonction qui a pour objectif d’afficher du texte dans la console : la fonction printf() .

Première approche

Un exemple valant mieux qu’un long discours, voici un premier exemple.

#include <stdio.h>
int main(void)
{
    printf("Bonjour tout le monde !\n");
    return 0;
}
Résultat
Bonjour tout le monde !

Deux remarques au sujet de ce code.

#include <stdio.h>

Il s’agit d’une directive du préprocesseur, facilement reconnaissable car elles commencent toutes par le symbole # . Celle-ci sert à inclure un fichier (« stdio.h ») qui contient les références de différentes fonctions d’entrée et sortie (« stdio » est une abréviation pour « Standard input output », soit « Entrée-sortie standard »).

Un fichier se terminant par l’extension « .h » est appelé un fichier d’en-tête ( header en anglais) et fait partie avec d’autres d’un ensemble plus large appelée la bibliothèque standard (« standard » car elle est prévue par la norme 1).

printf("Bonjour tout le monde !\n");

Ici, nous appelons la fonction printf() (un appel de fonction est toujours suivi d’un groupe de parenthèses) avec comme argument (ce qu’il y a entre les parenthèses de l’appel) un texte (il s’agit plus précisément d’une chaîne de caractères, qui est toujours comprise entre deux guillemets double). Le \n est un caractère spécial qui représente un retour à la ligne, cela est plus commode pour l’affichage.

Le reste devrait vous être familier.

Les formats

Bien, nous savons maintenant afficher une phrase, mais ce serait quand même mieux de pouvoir voir les valeurs de nos variables. Comment faire ? Hé bien, pour y parvenir, la fonction printf() met à notre disposition des formats. Ceux-ci sont en fait des sortes de repères au sein d’un texte qui indique à printf() que la valeur d’une variable est attendue à cet endroit. Voici un exemple pour une variable de type int .

#include <stdio.h>
int main(void)
{
    int variable = 20;
    printf("%d\n", variable);
    return 0;
}
Résultat
20

Nous pouvons voir que le texte de l’exemple précédent a été remplacé par %d , seul le \n a été conservé. Un format commence toujours par le symbole % et est suivi par une ou plusieurs lettres qui indiquent le type de données que nous souhaitons afficher. Cette suite de lettre est appelée un indicateur de conversion. En voici une liste non exhaustive.

Type | Indicateur(s) de conversion
_Bool | d (ou i)
char | c
signed char | d (ou i)
short | d (ou i)
int | d (ou i)
long | ld (ou li)
long long | lld (ou lli)
unsigned char | u, x (ou X) ou o
unsigned short | u, x (ou X) ou o
unsigned int | u, x (ou X) ou o
unsigned long | lu, lx (ou lX) ou lo
unsigned long long | llu, llx (ou llX) ou llo
float | f, e (ou E) ou g (ou G)
double | f, e (ou E) ou g (ou G)
long double | Lf, Le (ou LE) ou Lg (ou LG)

Notez que les indicateurs de conversions sont identiques pour les types _Bool , char (s’il stocke un entier), short et int (pareil pour leurs équivalents non signés) ainsi que pour les types float et double .

Les indicateurs x , X et o permettent d’afficher un nombre en représentation hexadécimale ou octale (l’indicateur x affiche les lettres en minuscules alors que l’indicateur X les affiches en majuscules).

Les indicateurs f , e et g permettent quant à eux d’afficher un nombre flottant. L’indicateur f affiche un nombre en notation simple avec, par défaut, six décimales ; l’indicateur e affiche un nombre flottant en notation scientifique (l’indicateur e utilise la lettre « e » avant l’exposant alors que l’indicateur « E » emploie la lettre « E ») et l’indicateur g choisit quant à lui entre les deux notations précédentes suivant le nombre fourni et supprime la partie fractionnaire si elle est nulle de sorte que l’écriture soit concise (la différence entre les indicateurs g et G est identique à celle entre les indicateurs e et E ).

Allez, un petit exemple pour reprendre tout cela et retravailler le chapitre précédent par la même occasion.

#include <stdio.h>
int main(void)
{
    char z = 'z';
    char a = 10;
    unsigned short b = 20;
    int c = 30;
    long d = 40;
    float e = 50.;
    double f = 60.0;
    long double g = 70.0;
    printf("%c\n", z);
    printf("%d\n", a);
    printf("%u\n", b);
    printf("%o\n", b);
    printf("%x\n", b);
    printf("%d\n", c);
    printf("%li\n", d);
    printf("%f\n", e);
    printf("%e\n", f);
    g = 80.0;
    printf("%Lg\n", g);
    return 0;
}
Résultat
z
10
20
24
14
30
40
50.000000
6.000000e+01
80

Si vous souhaitez afficher le caractère % vous devez le doubler : %% .

Précision des nombres flottants

Vous avez peut-être remarqué que lorsqu’un flottant est affiché avec le format f , il y a un certain nombre de zéros qui suivent (par défaut six) et ce, peu importe qu’ils soient utiles ou non. Afin d’en supprimer certains, vous pouvez spécifier une précision. Celle-ci correspond au nombre de chiffre suivant la virgule qui seront affichés. Elle prend la forme d’un point suivi par un nombre : la quantité de chiffres qu’il y aura derrière la virgule.

double x = 42.42734;
printf("%.2f", x);
Résultat
42.43

Les caractères spéciaux

Dans certains cas, nous souhaitons obtenir un résultat à l’affichage (saut de ligne, une tabulation, un retour chariot, etc.). Cependant, ils ne sont pas particulièrement pratiques à insérer dans une chaîne de caractères. Aussi, le C nous permet de le faire en utilisant une séquence d’échappement. Il s’agit en fait d’une suite de caractères commençant par le symbole \ et suivie d’une lettre. En voici une liste non exhaustive.

Séquence d’échappement | Signification
\a | Caractère d’appel
\b | Espacement arrière
\f | Saut de page
\n | Saut de ligne
\r | Retour chariot
\t | Tabulation horizontale
\v | Tabulation verticale
\" | Le symbole « " »
\\ | Le symbole « \ » lui-même

En général, vous n’utiliserez que le saut de ligne, la tabulation horizontale et de temps à autre le retour chariot, les autres n’ont quasiment plus d’intérêt. Un petit exemple pour illustrer leurs effets.

#include <stdio.h>
int main(void)
{
    printf("Quelques sauts de ligne\n\n\n");
    printf("\tIl y a une tabulation avant moi !\n");
    printf("Je voulais dire que...\r");
    printf("Hey ! Vous pourriez me laisser parler !\n");
    return 0;
}
Résultat
Quelques sauts de ligne
        Il y a une tabulation avant moi !
Hey ! Vous pourriez me laisser parler !

Le retour chariot provoque un retour au début de la ligne courante. Ainsi, il est possible d’écrire par-dessus un texte affiché.

Sur plusieurs lignes

Notez qu’il est possible d’écrire un long texte sans appeler plusieurs fois la fonction printf() . Pour ce faire, il suffit de le diviser en plusieurs chaînes de caractères.

#include <stdio.h>
int main(void)
{
    printf("Texte écrit sur plusieurs "
           "lignes dans le code source "
           "mais sur une seule dans la console.\n");
    return 0;
}
Résultat
Texte écrit sur plusieurs lignes dans le code source mais sur une seule dans la console.
  1. bibliothèque standard ↩

  2. ISO/IEC 9899:201x, doc. N1570, § 7, Library, p. 180.

    ↩

Interagir avec l'utilisateur

Maintenant que nous savons déclarer, utiliser et même afficher des variables, nous sommes fin prêts pour interagir avec l’utilisateur. En effet, jusqu’à maintenant, nous nous sommes contentés d’afficher des informations. Nous allons à présent voir comment en récupérer grâce à la fonction scanf() , dont l’utilisation est assez semblable à printf() .

#include <stdio.h>
int main(void)
{
    int age;
    printf("Quel âge avez-vous ? ");
    scanf("%d", &age);
    printf("Vous avez %d an(s)\n", age);
    return 0;
}
Résultat
Quel age avez-vous ? 15
Vous avez 15 an(s).

Comme vous le voyez, l’appel à scanf() ressemble très fort à celui de printf() mise à part l’absence du caractère spécial \n (qui n’a pas d’intérêt puisque nous récupérons des informations) et le symbole & .

À son sujet, souvenez-vous de la brève explication sur la mémoire au début du chapitre précédent. Celle-ci fonctionne comme une armoire avec des tiroirs (les adresses mémoires) et des objets dans ces tiroirs (nos variables). La fonction scanf() a besoin de connaitre l’emplacement en mémoire de nos variables afin de les modifier. Afin d’effectuer cette opération, nous utilisons le symbole & (qui est en fait un opérateur que nous verrons en détail plus tard). Ce concept de transmission d’adresses mémoires est un petit peu difficile à comprendre au début, mais ne vous inquiétez pas, vous aurez l’occasion de bien revoir tout cela en profondeur dans le chapitre traitant des pointeurs.

Ici, scanf() attend patiemment que l’utilisateur saisisse un nombre au clavier afin de modifier la valeur de la variable age . On dit que c’est une fonction bloquante, car elle suspend l’exécution du programme tant que l’utilisateur n’a rien entré.

Pour ce qui est des indicateurs de conversions, ils sont un peu différents de ceux de printf() .

Type | Indicateur(s) de conversion
char | c
signed char | hhd (ou hhi)
short | hd ou hi
int | d ou i
long | ld ou li
long long | lld ou lli
unsigned char | hhu, hhx ou hho
unsigned short | hu, hx ou ho
unsigned int | u, x ou o
unsigned long | lu, lx ou lo
unsigned long long | llu, llx, ou llo
float | f
double | lf
long double | Lf

Faites bien attention aux différences ! Si vous utilisez le mauvais format, le résultat ne sera pas celui que vous attendez. Les changements concernent les types char (s’il stocke un entier), short et double .

Notez que :

  • l’indicateur c ne peut être employé que pour récupérer un caractère et non un nombre ;
  • il n’y a plus qu’un seul indicateur pour récupérer un nombre hexadécimal : x (l’utilisation de lettres majuscules ou minuscules n’a pas d’importance) ;
  • il n’y a pas d’indicateur pour le type _Bool .

En passant, sachez qu’il est possible de lire plusieurs entrées en même temps, par exemple comme ceci.

int x, y;
scanf("%d %d", &x, &y);
printf("x = %d | y = %d\n", x, y);

L’utilisateur a deux possibilités, soit insérer un (ou plusieurs) espace(s) entre les valeurs, soit insérer un (ou plusieurs) retour(s) à la ligne entre les valeurs.

Résultat (1)
14
6
x = 14 | y = 6
Résultat (2)
14 6
x = 14 | y = 6

La fonction scanf() est en apparence simple (oui, en apparence ), mais son utilisation peut devenir très complexe lorsqu’il est nécessaire de vérifier les entrées de l’utilisateur (entre autres). Cependant, à votre niveau, vous ne pouvez pas encore effectuer de telles vérifications. Ce n’est toutefois pas très grave, nous verrons cela en temps voulu. ;)

Maintenant, vous êtes capable de communiquer avec l’utilisateur. Cependant, nos actions sont encore un peu limitées. Nous verrons dans les prochains chapitres comment mieux interagir avec l’utilisateur.

En résumé
  1. Les fonctions de la bibliothèque standard sont utilisables en incluant des fichiers d’en-tête ;
  2. Il est possible d’afficher du texte à l’aide de la fonction printf() ;
  3. La fonction printf() emploie différents indicateurs de conversion suivant le type de ses arguments ;
  4. Il existe un certain nombre de caractères spéciaux permettant de produire différents effets à l’affichage ;
  5. La fonction scanf() permet de récupérer une entrée ;
  6. Les indicateurs de conversions ne sont pas identiques entre printf() et scanf() ;
  7. L’opérateur & permet de transmettre l’adresse d’une variable à scanf() .


Les opérations mathématiques - Le langage C • Tutoriels • Zeste de Savoir

Les opérations mathématiques

Les opérations mathématiques de base

Jusqu’à présent, nous nous sommes contenté d’afficher du texte et de manipuler très légèrement les variables. Voyons à présent comment nous pouvons réaliser quelques opérations de base. Le langage C nous permet d’en réaliser cinq :

  • l’addition (opérateur + ) ;
  • la soustraction (opérateur - ) ;
  • la multiplication (opérateur * ) ;
  • la division (opérateur / ) ;
  • le modulo (opérateur % ).

Le langage C fournit bien entendu d’autres fonctions mathématiques et d’autres opérateurs, mais il est encore trop tôt pour vous les présenter.

Division et modulo

Les quatre premières opérations vous sont connues depuis l’école primaire. Cependant, une chose importante doit être précisée concernant la division : quand les deux nombres manipulés sont des entiers, il s’agit d’une division entière. Autrement dit, le quotient sera un entier et il peut y avoir un reste. Par exemple, 15 ÷ 6, ne donnera pas 2,5 (division réelle), mais un quotient de 2, avec un reste de 3.

printf("15 / 6 = %d\n", 15 / 6);
Résultat
15 / 6 = 2

Le modulo est un peu le complément de la division entière : au lieu de donner le quotient, il renvoie le reste d’une division euclidienne. Par exemple, le modulo de 15 par 6 est 3, car 15 = 2 × 6 + 3.

printf("15 % 6 = %d\n", 15 % 6);
Résultat
15 % 6 = 3

Avec des flottants, la division se comporte autrement et n’est pas une division avec reste. La division de deux flottants donnera un résultat « exact », avec potentiellement plusieurs chiffres après la virgule.

printf("15 / 6 = %f\n", 15. / 6.);   /* En C, ce n’est pas la même chose que 15 / 6 */
Résultat
15 / 6 = 2.500000

Utilisation

Il est possible d’affecter le résultat d’une expression contenant des calculs à une variable, comme lorsque nous les utilisons comme argument de printf() .

#include <stdio.h>
int main(void)
{
    int somme = 5 + 3;
    printf("5 + 3 = %d\n", somme);
    return 0;
}
Résultat
5 + 3 = 8

Toute opération peut manipuler :

  • des constantes ;
  • des variables ;
  • les deux à la fois.

Voici un exemple avec des constantes.

#include <stdio.h>
int main(void)
{
    printf("2 + 3 = %d\n", 2 + 3);
    printf("8 - 12 = %d\n", 8 - 12);
    printf("6 x 7 = %d\n", 6 * 7);
    printf("11 % 4 = %d\n", 11 % 4);
    return 0;
}
Résultat
2 + 3 = 5
8 - 12 = -4
6 x 7 = 42
11 % 4 = 3

Un autre avec des variables.

int a = 5;
int b = 3;
int somme = a + b;
printf("%d + %d = %d\n", a, b, somme);
Résultat
5 + 3 = 8

Et enfin, un exemple qui mélange variables et constantes.

int a = 5;
int b = 65;
printf("%d\n", b / a * 2 + 7 % 2);
Résultat
27

La priorité des opérateurs

Dans l’exemple précédent, nous avons utilisé plusieurs opérations dans une même ligne de code, une même expression. Dans ces cas-là, faites attention à la priorité des opérateurs ! Comme en mathématiques, certains opérateurs passent avant d’autres : les opérateurs * / % ont une priorité supérieure par rapport aux opérateurs + - .

Dans le code ci-dessous, c’est c * 4 qui sera exécuté d’abord, puis b sera ajouté au résultat. Faites donc attention sous peine d’avoir de mauvaises surprises. Dans le doute, ajoutez des parenthèses.

a = b + c * 4;

Les expressions

Il est possible de combiner opérateur, variables et constantes pour former des expressions, des lignes de code qui sont évaluées et produisent un résultat. Les lignes de code suivantes sont toutes des expressions.

"Bonjour !"
2 + 3
10 > 2

Généralement, une expression ne peut être écrite seule et doit faire partie d’une instruction. La frontière entre instruction et expression est assez floue puisqu’une instruction peut être composée de nombreuses expressions. Le code ci-dessous est un exemple d’instruction qui est quasi une expression (on parle d’expression-instruction).

x = 2 + 3;

Nous donnons en effet un ordre à l’ordinateur (« affecte la valeur 2 + 3 à x »), mais c’est aussi une expression qui produit la valeur 5 comme résultat. Vous verrez qu’en C, la majorité des lignes de code sont des instructions-expressions. C’est ce qui est appelé la programmation impérative. C’est le choix des concepteurs du langage, mais ce n’est pas la seule possibilité (il en existe d’autres, mais ça ne nous concerne pas en tant qu’utilisateurs du C).

Type d’une expression

Vous avez sans doute remarqué que nous avons utilisé directement des expressions ( 2 + 3 par exemple) comme argument de la fonction printf() . Rien de bien surprenant me direz-vous… À un détail près : quel indicateur de format doit-on utiliser ? Autrement dit, quel est le type d’une expression ? D’ailleurs, ont-elles un type ?

Hé bien, oui. Tout comme les variables, les expressions ont un type. Ce dernier dépend toutefois du type des éléments qui la compose. En l’occurrence, une constante entière comme 2 ou 3 est par défaut de type int . Le résultat d’une somme, par exemple, sera donc également de type int . Les constantes flottantes comme 5. ou 78.0 sont, elles, de type double et le résultat d’une opération sera alors de type double .

D’accord, mais si j’additionne un int avec un double , cela me donne quoi ?

Heureusement pour nous, la norme 1 a prévu ces différents cas et a fixé des règles :

  1. si un opérande est de type long double , le résultat est de type long double ;
  2. si un opérande est de type double , le résultat est de type double ;
  3. si un opérande est de type float , le résultat est de type float ;
  4. si un opérande est de type unsigned long long , le résultat est de type unsigned long long ;
  5. si un opérande est de type long long , le résultat est de type long long ;
  6. si un opérande est de type unsigned long , le résultat est de type unsigned long ;
  7. si un opérande est de type long , le résultat est de type long ;
  8. si un opérande est de type unsigned int , le résultat est de type unsigned int ;
  9. le résultat est de type int .

Type d’une constante

Heu … D’accord, mais vous venez de dire que les constantes entières étaient de type int et que les constantes flottantes étaient de type double . Du coup, je fais comment pour obtenir une constante d’un autre type ?

À l’aide d’un suffixe. Celui-ci se place à la fin de la constante et permet de modifier son type. En voici la liste complète.

Type | Suffixe
u ou U | unsigned
l ou L | long
ll ou LL | long long
f ou F | float
l ou L | long double

Notez que les suffixes L (ou l ) et U (ou u ) peuvent être combinés.

Faites attention que le suffixe L n’a pas le même effet suivant qu’il est apposé à une constante entière ou réelle. Dans le premier cas la constante sera de type long alors que dans le second elle sera de type long double .

Exemple

Allez, un petit récapitulatif.

#include <stdio.h>
int main(void)
{
    /* long double + int = long double */
    printf("78.56 + 5 = %Lf\n", 78.56L + 5);
    /* long + double = double */
    printf("5678 + 2.2 = %f\n", 5678L + 2.2);
    /* long + unsigned long = unsigned long */
    printf("2 + 5 = %lu\n", 2L + 5UL);
    /* long long + int = long long */
    printf("1 + 1 = %lld\n", 1LL + 1);
    return 0;
}
Résultat
78.56 + 5 = 83.560000
5678 + 2.2 = 5680.200000
2 + 5 = 7
1 + 1 = 2

Nous vous conseillons d’opter pour les lettres majuscules qui ont l’avantage d’être plus lisibles.

Conversions de types

La conversion de type est une opération qui consiste à changer le type de la valeur d’une expression. Ainsi, il vous est par exemple possible de convertir une valeur de type float en type int .

Perte d’informations

Une perte d’informations survient quand le type d’une variable est converti vers un autre type ayant une capacité plus faible et que celui-ci ne peut pas contenir la valeur d’origine. Si, par exemple, nous convertissons un double de cent chiffres en un int , il y a perte d’informations, car le type int ne peut pas contenir un nombre de cent chiffres. Retenez donc bien cette assertion : une conversion d’un type T vers un type S de plus faible capacité entraîne une perte d’informations (une perte de précision pour les nombres).

Les conversions peuvent être vicieuses et doivent être manipulées avec précaution, au risque de tomber sur des valeurs fausses en cas de perte d’informations. Nous découvrirons d’ici quelques chapitres comment connaître la taille d’un type pour éviter ces pertes d’informations.

Deux types de conversions

Il existe deux types de conversions : les conversions explicites et les conversions implicites.

Les conversions explicites

Les conversions explicites sont des conversions demandées par le programmeur. Elles s’utilisent suivant ce modèle.

(<Type>)<Expression>

Voici par exemple un code où nous demandons explicitement la conversion d’un double en int .

int a;
const double pi = 3.14;
a = (int)pi;

La valeur de pi reste inchangée, elle vaudra toujours 3.14 dans la suite du programme. Par contre, a vaut maintenant 3, puisque la valeur de pi a été convertie en int .

Conversions implicites

Les conversions implicites sont des conversions spécifiées par la norme et réalisées automatiquement par le compilateur. En général, cela ne pose pas de problèmes et cela est même désirable. Par exemple, il y a toujours une conversion implicite dans le cadre d’une affectation.

Ainsi, la conversion explicite du code précédent n’est en fait pas nécessaire.

int a;
const double pi = 3.14;
/* Il y a conversion implicite du type double vers le type int. */
a = pi;

  1. ISO/IEC 9899:201x, doc. N1570, § 6.3.1.8, Usual arithmetic conversions, p. 52.

    ↩

Sucre syntaxique

Dans les expressions vues au-dessus, nous avons utilisé des affectations pour sauvegarder le résultat de l’opération dans une variable. Les expressions obtenues ainsi sont assez longues et on peut se demander s’il existe des moyens pour écrire moins de code. Hé bien, le langage C fournit des écritures pour se simplifier la vie. Certains cas particuliers peuvent s’écrire avec des raccourcis, du « sucre syntaxique ».

Les opérateurs combinés

Comment vous y prendriez-vous pour multiplier une variable par trois ? La solution qui devrait vous venir à l’esprit serait d’affecter à la variable son ancienne valeur multipliée par trois.

int variable = 3;
variable = variable * 3;
printf("variable * 3 = %d\n", variable);
Résultat
variable * 3 = 9

Ce qui est parfaitement correct. Cependant, cela implique de devoir écrire deux fois le nom de la variable, ce qui est quelque peu pénible et source d’erreurs. Aussi, il existe des opérateurs combinés qui réalisent une affectation et une opération en même temps.

Opérateur combiné | Équivalent à
variable += nombre | variable = variable + nombre
variable -= nombre | variable = variable - nombre
variable *= nombre | variable = variable * nombre
variable /= nombre | variable = variable / nombre
variable %= nombre | variable = variable % nombre

Avec le code précédent, nous obtenons ceci.

int variable = 3;
variable *= 3;
printf("variable * 3 = %d\n", variable);
Résultat
variable * 3 = 9

L’incrémentation et la décrémentation

L’incrémentation et la décrémentation sont deux opérations qui, respectivement, ajoute ou enlève une unité à une variable. Avec les opérateurs vu précédemment, cela se traduit par le code ci-dessous.

variable += 1; /* Incrémentation */
variable -= 1; /* Décrémentation */

Cependant, ces deux opérations étant très souvent utilisées, aussi elles ont droit chacune à un opérateur spécifique disponible sous deux formes :

  • une forme préfixée ;
  • une forme suffixée.

La forme préfixée s’écrit comme ceci.

++variable; /* Incrémentation */
--variable; /* Décrémentation */

La forme suffixée s’écrit comme cela.

variable++; /* Incrémentation */
variable--; /* Décrémentation */

Le résultat des deux paires d’opérateurs est le même : la variable variable est incrémentée ou décrémentée, à une différence près : le résultat de l’opération.

  1. Dans le cas de l’opérateur préfixé ( --variable ou ++variable ), le résultat sera la valeur de la variable augmentée ou diminuée d’une unité.
  2. Dans le cas de l’opérateur suffixé ( variable-- ou variable++ ), le résultat sera la valeur de la variable.

Illustration !

#include <stdio.h>
int main(void)
{
    int x = 1;
    int y = 1;
    int a = x++;
    int b = ++y;
    printf("a = %d\n", a);
    printf("b = %d\n", b);
    printf("x = %d\n", x);
    printf("y = %d\n", y);
    return 0;
}
Résultat
a = 1
b = 2
x = 2
y = 2

Comme vous pouvez le constater, la valeur de l’expression x++ est 1 alors que la valeur de l’expression ++y est 2. Cela étant, dans les deux cas, les variables x et y ont bien été incrémentées.

Exercices

Vous êtes prêts pour un exercice ?

Essayez de réaliser une minicalculatrice qui :

  • dit « bonjour » ;
  • demande deux nombres entiers à l’utilisateur ;
  • les additionne, les soustrait, les multiplie et les divise (au millième près) ;
  • dit « au revoir ».

Un exemple d’utilisation pourrait être celui-ci.

Bonjour !
Veuillez saisir le premier nombre : 4
Veuillez saisir le deuxième nombre : 7
Calculs :
        4 + 7 = 11
        4 - 7 = -3
        4 * 7 = 28
        4 / 7 = 0.571
Au revoir !

Bien, vous avez maintenant toutes les cartes en main, donc : au boulot ! :)

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
   int a;
   int b;
   printf("Bonjour !\n");
   /* Nous demandons deux nombres à l'utilisateur */
   printf("Veuillez saisir le premier nombre : ");
   scanf("%d", &a);
   printf("Veuillez saisir le deuxième nombre : ");
   scanf("%d", &b);
   /* Puis nous effectuons les calculs */
   printf("Calculs :\n");
   printf("\t%d + %d = %d\n", a, b, a + b);
   printf("\t%d - %d = %d\n", a, b, a - b);
   printf("\t%d * %d = %d\n", a, b, a * b);
   printf("\t%d / %d = %.3f\n", a, b, a / (double)b);
   printf("Au revoir !\n");
   return 0;
}

Vous y êtes arrivé sans problèmes ? Bravo ! Dans le cas contraire, ne vous inquiétiez pas, ce n’est pas grave. Relisez bien tous les points qui ne vous semblent pas clairs et ça devrait aller mieux.

Dans le chapitre suivant, nous nous pencherons sur les conditions.

En résumé

  1. Le C fournit des opérateurs pour réaliser les opérations mathématiques de base ;
  2. Dans le cas où une division est effectuée entre des nombres entiers, le quotient sera également un entier ;
  3. La priorité des opérateurs est identique à celle décrite en mathématiques ;
  4. En cas de mélange des types dans une expression, le C prévoit des règles de conversions ;
  5. Le type d’une constante peut-être modifié à l’aide de suffixes ;
  6. Il est possible de convertir une expression d’un type vers un autre, certaines conversions sont implicites ;
  7. Le C fourni des opérateurs combinés et des opérateurs pour l’incrémentation et la décrémentation afin de gagner en concision.


Tests et conditions - Le langage C • Tutoriels • Zeste de Savoir

Tests et conditions

Les booléens

Comme les opérateurs que nous avons vu précédemment ( + , - , * , etc), les opérateurs de comparaison et les opérateurs logiques donnent un résultat : « vrai » si la condition est vérifiée, et « faux » si la condition est fausse. Toutefois, comme vous le savez, notre ordinateur ne voit que des nombres. Aussi, il est nécessaire de représenter ces valeurs à l’aide de ceux-ci.

Certains langages fournissent pour cela un type distinct pour stocker le résultat des opérations de comparaison et deux valeurs spécifiques : true (vrai) et false (faux). Néanmoins, dans les premières versions du langage C, ce type spécial n’existait pas (le type _Bool a été introduit par la norme C99). Il a donc fallu ruser et trouver une solution pour représenter les valeurs « vrai » et « faux ». Pour cela, la méthode la plus simple a été privilégiée : utiliser directement des nombres pour représenter ces deux valeurs. Ainsi, le langage C impose que :

  • la valeur « faux » soit représentée par zéro ;
  • et que la valeur « vrai » soit représentée par tout sauf zéro.

Les opérateurs de comparaison et les opérateurs logiques suivent cette convention pour représenter leur résultat. Dès lors, une condition vaudra 0 si elle est fausse et 1 si elle est vraie.

Notez qu’il existe un en-tête <stdbool.h> qui fournit deux constantes entières : true (qui vaut 1) et false , qui vaut 0 ainsi qu’un synonyme pour le type _Bool nommé bool .

#include <stdbool.h>
#include <stdio.h>
int
main(void)
{
    bool booleen = true;
    printf("true = %u, false = %u\n", true, false);
    printf("booleen = %u\n", booleen);
    return 0;
}
Résultat
true = 1, false = 0
booleen = 1

Les opérateurs de comparaison

Le langage C fournit différents opérateurs qui permettent d’effectuer des comparaisons entre des nombres. Ces opérateurs peuvent s’appliquer aussi bien à des constantes qu’à des variables (ou un mélange des deux). Ces derniers permettent donc par exemple de vérifier si une variable est supérieure à une autre, si elles sont égales, etc.

Comparaisons

L’écriture de conditions est similaire aux écritures mathématiques que vous voyez en cours : l’opérateur est entre les deux expressions à comparer. Par exemple, dans le cas de l’opérateur > (« strictement supérieur à »), il est possible d’écrire des expressions du type a > b , qui vérifie si la variable a est strictement supérieure à la variable b .

Le tableau ci-dessous reprend les différents opérateurs de comparaisons.

Opérateur | Signification
== | Égalité
!= | Inégalité
< | Strictement inférieur à
<= | Inférieur ou égal à
> | Strictement supérieur à
>= | Supérieur ou égal à

Ces opérateurs ne semblent pas très folichons. En effet, avec, nous ne pouvons faire que quelques tests basiques sur des nombres. Cependant, rappelez-vous : pour un ordinateur, tout n’est que nombre et comme pour le stockage des données (revoyez le début du chapitre sur les variables si cela ne vous dit rien) il est possible de ruser et d’exprimer toutes les conditions possibles avec ces opérateurs (cela vous paraîtra plus clair quand nous passerons aux exercices).

Résultat d’une comparaison

Comme dit dans l’extrait plus haut, une opération de comparaison va donner zéro si elle est fausse et un si elle est vraie. Pour illustrer ceci, vérifions quels sont les résultats donnés par différentes comparaisons.

int main(void)
{
    printf("10 == 20 vaut %d\n", 10 == 20);
    printf("10 != 20 vaut %d\n", 10 != 20);
    printf("10 < 20 vaut %d\n", 10 < 20);
    printf("10 > 20 vaut %d\n", 10 > 20);
    return 0;
}
Résultat
10 == 20 vaut 0
10 != 20 vaut 1
10 < 20 vaut 1
10 > 20 vaut 0

Le résultat confirme bien ce que nous avons dit ci-dessus.

Les opérateurs logiques

Toutes ces comparaisons sont toutefois un peu faibles seules car il y a des choses qui ne sont pas possibles à vérifier en utilisant une seule comparaison. Par exemple, si un nombre est situé entre zéro et mille (inclus). Pour ce faire, il serait nécessaire de vérifier que celui-ci est supérieur ou égal à zéro et inférieur ou égal à mille.

Il nous faudrait donc trouver un moyen de combiner plusieurs comparaisons entre elles pour résoudre ce problème. Hé bien rassurez-vous, le langage C fournit de quoi associer plusieurs résultats de comparaisons : les opérateurs logiques.

Les opérateurs logiques de base

Il existe trois opérateurs logiques. L’opérateur « et », l’opérateur « ou », et l’opérateur de négation. Les deux premiers permettent de combiner deux conditions alors que le troisième permet d’inverser le sens d’une condition.

L’opérateur « et »

L’opérateur « et » va manipuler deux conditions. Il va donner un si elles sont vraies, et zéro sinon.

Première condition | Seconde condition | Résultat de l’opérateur « et »
Fausse | Fausse | 0
Fausse | Vraie | 0
Vraie | Fausse | 0
Vraie | Vraie | 1

Cet opérateur s’écrit && et s’intercale entre les deux conditions à combiner. Si nous reprenons l’exemple vu plus haut, pour combiner les comparaisons a >= 0 et a <= 1000 , nous devons placer l’opérateur && entre les deux, ce qui donne l’expression a >= 0 && a <= 1000 .

L’opérateur « ou »

L’opérateur « ou » fonctionne exactement comme l’opérateur « et », il prend deux conditions et les combine pour former un résultat. Cependant, l’opérateur « ou » vérifie que l’une des deux conditions (ou que les deux) est (sont) vraie(s).

Première condition | Seconde condition | Résultat de l’opérateur « ou »
Fausse | Fausse | 0
Fausse | Vraie | 1
Vraie | Fausse | 1
Vraie | Vraie | 1

Cet opérateur s’écrit || et s’intercale entre les deux conditions à combiner. L’exemple suivant permet de déterminer si un nombre est divisible par trois ou par cinq (ou les deux) : (a % 3 == 0) || (a % 5 == 0) . Notez que les parenthèses ont été placées par soucis de lisibilité.

L’opérateur de négation

Cet opérateur est un peu spécial : il manipule une seule condition et en inverse le sens.

Condition | Résultat de l’opérateur de négation
Fausse | 1
Vraie | 0

Cet opérateur se note ! . Son utilité ? Simplifier certaines expressions. Par exemple, si nous voulons vérifier cette fois qu’un nombre n’est pas situé entre zéro et mille, nous pouvons utiliser la condition a >= 0 && a <= 1000 et lui appliquer l’opérateur de négation, ce qui donne !(a >= 0 && a <= 1000) .

Faites bien attention à l’utilisation des parenthèses ! L’opérateur de négation s’applique à l’opérande le plus proche (sans parenthèses, il s’agirait de a ). Veillez donc a bien entourer de parenthèses l’expression concernée par la négation.

Notez que pour cet exemple, il est parfaitement possible de se passer de cet opérateur à l’aide de l’expression a < 0 || a > 1000 . Il est d’ailleurs souvent possible d’exprimer une condition de différentes manières.

Évaluation en court-circuit

Les opérateurs && et || evaluent toujours la première condition avant la seconde. Cela paraît évident, mais ce n’est pas le cas dans tous les langages de programmation. Ce genre de détail permet à ces opérateurs de disposer d’un comportement assez intéressant : l’évaluation en court-circuit.

De quoi s’agit-il ? Pour illustrer cette notion, reprenons l’exemple précédent : nous voulons vérifier si un nombre est compris entre zéro et mille, ce qui donne l’expression a >= 0 && a <= 1000 . Si jamais a est inférieur à zéro, nous savons dès la vérification de la première condition qu’il n’est pas situé dans l’intervalle voulu. Il n’est donc pas nécessaire de vérifier la seconde. Hé bien, c’est exactement ce qu’il se passe en langage C. Si le résultat de la première condition suffit pour déterminer le résultat de l’opérateur && ou || , alors la seconde condition n’est pas évaluée . Voilà pourquoi l’on parle d’évaluation en court-circuit.

Plus précisément, ce sera le cas pour l’opérateur && si la première condition est fausse et pour l’opérateur || si la première condition est vraie (relisez les tableaux précédents si cela ne vous semble pas évident).

Ce genre de propriété peut-être utilisé efficacement pour éviter de faire certains calculs, en choisissant intelligemment quelle sera la première condition.

Combinaisons

Bien sûr, il est possible de mélanger ces opérateurs pour créer des conditions plus complexes. Voici un exemple un peu plus long (et inutile, soit dit en passant :-° ).

int a = 3;
double b = 64.67;
double c = 12.89;
int d = 8;
int e = -5;
int f = 42;
int r;
r = ((a < b && b > 32) || (c < d + b || e == 0)) && (f > d);
printf("La valeur logique est égale à : %d\n", r);

Ici, la variable r est égale à 1, la condition est donc vraie.

Priorité des opérations

Comme pour les opérateurs mathématiques, les opérateurs logiques et de comparaisons ont des règles de priorité (revoyez le chapitre sur les opérations mathématiques si cela ne vous dit rien). En regardant le code écrit plus haut, vous avez d’ailleurs sûrement remarqué la présence de plusieurs parenthèses. Celles-ci permettent d’enlever toute ambigüité dans les expressions créées avec des opérateurs logiques.

Les règles de priorités sont les suivantes, de la plus forte à la plus faible :

  1. l’opérateur de négation ( ! ) ;
  2. les opérateurs relationnels ( < , <= , > , >= ) ;
  3. les opérateurs d’égalité et d’inégalité ( == , != ) ;
  4. l’opérateur logique && ;
  5. l’opérateur logique || .

Ainsi, les expressions suivantes seront évaluées comme suit.

Expression | Ordre d’évaluation
10 < 20 == 0 | 10 < 20 , puis l’égalité
!a != 1 < 0 | !a , puis 1 < 0 , puis l’inégalité
a && b || c && d | a && b , puis c && d , puis le ||

Si vous souhaitez un résultat différent, il est nécessaire d’ajouter des parenthèses pour modifier les règles de priorité, par exemple comme suit a && (b || c) && d auquel cas l’expression a || c est evaluée la première, suivie du premier && , puis du second.

Au prochain chapitre, nous allons combiner les conditions avec une seconde notion : les sélections.

En résumé

  1. Une condition est soit vrai, soit fausse.
  2. Une condition fausse est représentée par une valeur nulle, une condition vraie par une valeur non nulle ;
  3. Les opérateurs && et || utilisent l’évaluation en court-circuit : si la première condition est suffisante pour déterminer le résultat, la seconde n’est pas évaluée.
  4. Les opérateurs de comparaisons et les opérateurs logiques ont également des règles de priorité.


Les sélections - Le langage C • Tutoriels • Zeste de Savoir

Les sélections

La structure if

Vous savez désormais manipuler des conditions, c’est bien, cependant l’intérêt de la chose reste assez limité pour l’instant. Rendons à présent cela plus intéressant en voyant comment exécuter un bloc d’instruction quand une ou plusieurs conditions sont respectées. C’est le rôle de l’instruction if et de ses consœurs.

L’instruction if

L’instruction if permet d’exécuter un bloc d’instructions si une condition est vérifiée ou de le passer si ce n’est pas le cas.

           |
     +-----v-----+   Faux
     | Condition |---------+
     +-----------+         |
           |               |
      Vrai |               |
           |               |
+----------v----------+    |
| Bloc d'instructions |    |
+---------------------+    |
           |               |
           |<--------------+
           |
           v
   Suite du programme

L’instruction if ressemble à ceci.

if (/* Condition */)
{
    /* Une ou plusieurs instruction(s) */
}

Si la condition n’est pas vérifiée, le bloc d’instructions est passé et le programme recommence immédiatement à la suite du bloc d’instructions délimité par l’instruction if .

Si vous n’avez qu’une seule instruction à réaliser, vous avez la possibilité de ne pas mettre d’accolades.

if (/* Condition */)
    /* Une seule instruction */

Cependant, nous vous conseillons de mettre les accolades systématiquement afin de vous éviter des problèmes si vous décidez de rajouter des instructions par la suite en oubliant d’ajouter des accolades. Bien sûr, ce n’est qu’un conseil, vous êtes libre de ne pas le suivre.

À présent, voyons quelques exemples d’utilisation.

Exemple 1

#include <stdio.h>
int main(void)
{
    int a = 10;
    int b = 20;
    if (a < b)
    {
        printf("%d est inférieur à %d\n", a, b);
    }
    return 0;
}
Résultat
10 est inférieur à 20

L’instruction if évalue l’expression logique a < b , conclut qu’elle est valide et exécute le bloc d’instructions.

Exemple 2

#include <stdio.h>
int main(void)
{
    int a = 10;
    int b = 20;
    if (a > b)
    {
        printf("%d est supérieur à %d\n", a, b);
    }
    return 0;
}

Ce code n’affiche rien. La condition étant fausse, le bloc contenant l’appel à la fonction printf() est ignoré.

La clause else

Avec l’instruction if , nous savons exécuter un bloc d’instructions quand une condition est remplie. Toutefois, si nous souhaitons réaliser une action en cas d’échec de l’évaluation de la condition, nous devons ajouter une autre instruction if à la suite, comme ci-dessous.

if (a > 5)
{
    /* Du code */
}
if (a <= 5)
{
    /* Code alternatif */
}

Le seul problème, c’est qu’il est nécessaire d’ajouter une instruction if et d’évaluer une nouvelle condition, ce qui n’est pas très efficace et assez long à taper. Pour limiter les dégâts, le C offre la possibilité d’ajouter une clause else , qui signifie « sinon ». Celle-ci se place immédiatement après le bloc d’une instruction if et permet d’exécuter un bloc d’instructions alternatif si la condition testée n’est pas vérifiée. Sa syntaxe est la suivante.

if (/* Condition */)
{
    /* Une ou plusieurs instructions */
}
else
{
    /* Une ou plusieurs instructions */
}

Et elle doit être comprise comme ceci.

           |
     +-----v-----+   Faux
     | Condition |--------------------+
     +-----+-----+                    |
           |                          |
      Vrai |                          |
           |                          |
+----------v----------+   +-----------v-----------+
| Bloc d'instructions |   | Bloc d'instructions   |
| de l'instruction if |   | de l'instruction else |
+----------+----------+   +-----------+-----------+
           |                          |
           |<-------------------------+
           |
           v
   Suite du programme

Exemple

Supposons que nous voulions créer un programme très simple auquel nous fournissons une heure et qui indique s’il fait jour ou nuit à cette heure-là. Nous supposerons qu’il fait jour de 8 heures à 20 heures et qu’il fait nuit sinon.

int main(void)
{
    int heure;
    scanf("%d", &heure);
    if (heure > 8 && heure < 20)
    {
        printf("Il fait jour.\n");
    }
    else
    {
        printf("Il fait nuit.\n");
    }
    return 0;
}
Résultat
10
Il fait jour.

if, else if

Il est parfois nécessaire d’imbriquer plusieurs instructions if else les unes dans les autres, par exemple comme suit.

if (/* Condition(s) */)
{
    /* Instruction(s) */
}
else
{
    if (/* Condition(s) */)
    {
        /* Instruction(s) */
    }
    else
    {
        if (/* Condition(s) */)
        {
            /* Instruction(s) */
        }
        else
        {
            /* Instruction(s) */
        }
    }
}

Ce qui est assez long et lourd à écrire, d’autant plus s’il y a beaucoup d’imbrications… Toutefois et heureusement pour nous, il est possible de simplifier cette écriture.

Tout d’abord, rappelez-vous : si un if ou un else ne comprend qu’une seule instruction, alors les accolades sont facultatives. Nous pouvons donc les retirer pour deux else (une suite if else compte pour une seule instruction).

if (/* Condition(s) */)
{
    /* Instruction(s) */
}
else
    if (/* Condition(s) */)
    {
        /* Instruction(s) */
    }
    else
        if (/* Condition(s) */)
        {
            /* Instruction(s) */
        }
        else
        {
            /* Instruction(s) */
        }

Ensuite, les retours à la ligne n’étant pas obligatoires, nous pouvons « coller » les deux if et les deux else qui se suivent.

if (/* Condition(s) */)
{
    /* Instruction(s) */
}
else if (/* Condition(s) */)
{
    /* Instruction(s) */
}
else if (/* Condition(s) */)
{
    /* Instruction(s) */
}
else
{
    /* Instruction(s) */
}

Notez que comme il s’agit toujours d’une suite d’instructions if else , il n’y aura qu’un seul bloc d’instructions qui sera finalement exécuté. En effet, l’ordinateur va tester la condition de l’instruction if , puis, si elle est fausse, celle de l’instruction if de la clause else et ainsi de suite jusqu’à ce qu’une condition soit vraie (ou jusqu’à une clause else finale si elles sont toutes fausses).

#include <stdio.h>
int main(void)
{
    int heure = 11;
    if (heure < 7)
    {
        printf("Zzz... \n");
    }
    else if (heure >= 7 && heure < 12)
    {
        printf("C'est le matin !\n");
    }
    else if (heure == 12)
    {
        printf("Il est midi !\n");
    }
    else if (heure > 12 && heure < 18)
    {
        printf("C'est l'après-midi !\n");
    }
    else if (heure >= 18 && heure < 24)
    {
        printf("C'est le soir !\n");
    }
    else if (heure == 24 || heure == 0)
    {
        printf("Il est minuit, dormez brave gens !\n");
    }
    else
    {
        printf("Il est l'heure de réapprendre à lire l'heure !\n");
    }
    return 0;
}
Résultat
11
On est le matin !
0
Il est minuit, dormez brave gens !
-2
Il est l'heure de réapprendre à lire l'heure !

Exercice

Imaginez que vous avez un score de jeu vidéo sous la main :

  • si le score est strictement inférieur à deux mille, affichez « C’est la catastrophe ! » ;
  • si le score est supérieur ou égal à deux mille et que le score est strictement inférieur à cinq mille, affichez : « Tu peux mieux faire ! » ;
  • si le score est supérieur ou égal à cinq mille et que le score est strictement inférieur à neuf mille, affichez : « Tu es sur la bonne voie ! » ;
  • sinon, affichez : « Tu es le meilleur ! ».

Au boulot ! :)

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
    int score;
    printf("Quel est le score du joueur ? ");
    scanf("%d", &score);
    if (score < 2000)
    {
        printf("C'est la catastrophe !\n");
    }
    else if (score >= 2000 && score < 5000)
    {
        printf("Tu peux mieux faire !\n");
    }
    else if (score >= 5000 && score < 9000)
    {
        printf("Tu es sur la bonne voie !\n");
    }
    else
    {
        printf("Tu es le meilleur !\n");
    }
    return 0;
}

L'instruction switch

L’instruction switch permet de comparer la valeur d’une expression par rapport à une liste de constantes entières. Techniquement, elle permet d’écrire de manière plus concise une suite d’instructions if else qui auraient pour objectif d’accomplir différentes actions suivant la valeur d’une expression.

if (a == 1)
{
    /* Instruction(s) */
}
else if (a == 2)
{
    /* Instruction(s) */
}
/* Etc. */
else
{
    /* Instruction(s) */
}

Avec l’instruction switch , cela donne ceci.

switch (a)
{
case 1:
    /* Instruction(s) */
    break;
case 2:
    /* Instruction(s) */
    break;
/* Etc... */
default: /* Si aucune comparaison n'est juste */
    /* Instruction(s) à exécuter dans ce cas */
    break;
}

Ici, la valeur de la variable a est comparée successivement avec chaque entrée de la liste, indiquées par le mot-clé case . En cas de correspondance, les instructions suivant le mot-clé case sont exécutées jusqu’à rencontrer une instruction break (nous la verrons plus en détail un peu plus tard). Si aucune comparaison n’est bonne, alors ce sont les instructions de l’entrée marquée avec le mot-clé default qui seront exécutées.

Exemple

#include <stdio.h>
int main(void)
{
    int note;
    printf("Quelle note as-tu obtenue (sur cinq) ? ");
    scanf("%d", &note);
    switch(note)
    {
    /* Si note == 0 */
    case 0:
        printf("No comment.\n");
        break;
    /* Si note == 1 */
    case 1:
        printf("Cela te fait 4/20, c'est accablant.\n");
        break;
    /* Si note == 2 */
    case 2:
        printf("On se rapproche de la moyenne, mais ce n'est pas encore ça.\n");
        break;
    /* Si note == 3 */
    case 3:
        printf("Tu passes.\n");
        break;
    /* Si note == 4*/
    case 4:
        printf("Bon travail, continue ainsi !\n");
        break;
    /* Si note == 5 */
    case 5:
        printf("Excellent !\n");
        break;
    /* Si note est différente de 0, 1, 2, 3, 4 et 5 */
    default:
        printf("Euh... tu possèdes une note improbable...\n");
        break;
    }
    return 0;
}

Notez que comme pour l’instruction else , une entrée marquée avec le mot-clé default n’est pas obligatoire.

Plusieurs entrées pour une même action

Une même suite d’instructions peut être désignée par plusieurs entrées comme le montre l’exemple suivant.

int main(void)
{
    int note;
    printf("Quelle note as-tu obtenue ? ");
    scanf("%d", &note);
    switch(note)
    {
    /* Si la note est comprise entre zéro et trois inclus */
    case 0:
    case 1:
    case 2:
    case 3:
        printf("No comment.\n");
        break;
    /* Si la note est comprise entre quatre et sept inclus */
    case 4:
    case 5:
    case 6:
    case 7:
        printf("C'est accablant.\n");
        break;
    /* Si la note est comprise entre huit et neuf inclus */
    case 8:
    case 9:
        printf("On se rapproche de la moyenne, mais ce n'est pas encore ça.\n");
        break;
    /* Si la note est comprise entre dix et douze inclus */
    case 10:
    case 11:
    case 12:
        printf("Tu passes.\n");
        break;
    /* Si la note est comprise entre treize et seize inclus */
    case 13:
    case 14:
    case 15:
    case 16:
        printf("Bon travail, continue ainsi !\n");
        break;
    /* Si la note est comprise entre dix-sept et vingt inclus */
    case 17:
    case 18:
    case 19:
    case 20:
        printf("Excellent !\n");
        break;
    /* Si la note est différente */
    default:
        printf("Euh... tu possèdes une note improbable...\n");
        break;
    }
    return 0;
}

Plusieurs entrées sans instruction break

Sachez également que l’instruction break n’est pas obligatoire. En effet, le but de cette dernière est de sortir du switch et donc de ne pas exécuter les actions d’autres entrées. Toutefois, il arrive que les actions à réaliser se chevauchent entre entrées auquel cas l’instruction break serait plutôt mal venue.

Prenons un exemple : vous souhaitez réaliser un programme qui affiche entre 1 à dix fois la même phrase, ce nombre étant fourni par l’utilisateur. Vous pourriez écrire une suite de if ou différentes entrées d’un switch qui, suivant le nombre entré, appelleraient une fois printf() , puis deux fois, puis trois fois, etc. mais cela serait horriblement lourd.

Dans un tel cas, une meilleure solution consiste à appeler printf() à chaque entrée du switch , mais de ne pas terminer ces dernières par une instruction break .

#include <stdio.h>
int
main(void)
{
    unsigned nb;
    printf("Combien de fois souhaitez-vous répéter l'affichage (entre 1 à 10 fois) ? ");
    scanf("%u", &nb);
    switch (nb)
    {
    case 10:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 9:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 8:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 7:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 6:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 5:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 4:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 3:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 2:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 1:
        printf("Cette phrase est répétée une à dix fois.\n");
    case 0:
        break;
    default:
        printf("Certes, mais encore ?\n");
        break;
    }
    return 0;
}
Résultat
Combien de fois souhaitez-vous répéter l'affichage (entre 1 à 10 fois) ? 2
Cette phrase est répétée une à dix fois.
Cette phrase est répétée une à dix fois.
Combien de fois souhaitez-vous répéter l'affichage (entre 1 à 10 fois) ? 5
Cette phrase est répétée une à dix fois.
Cette phrase est répétée une à dix fois.
Cette phrase est répétée une à dix fois.
Cette phrase est répétée une à dix fois.
Cette phrase est répétée une à dix fois.

Comme vous le voyez, la phrase « Cette phrase est répétée une à dix fois » est affichée une à dix fois suivant le nombre initialement fourni. Cela est possible étant donné l’absence d’instruction break entre les case 10 à 1, ce qui fait que l’exécution du switch continue de l’entrée initiale jusqu’au case 0.

L'opérateur conditionnel

L’opérateur conditionnel ou opérateur ternaire est un opérateur particulier dont le résultat dépend de la réalisation d’une condition. Son deuxième nom lui vient du fait qu’il est le seul opérateur du langage C à requérir trois opérandes : une condition et deux expressions.

(condition) ? expression si vrai : expression si faux

Les parenthèses entourant la condition ne sont pas obligatoires, mais préférables.

Grosso modo , cet opérateur permet d’écrire de manière condensée une structure if {} else {} . Voyez par vous-mêmes.

#include <stdio.h>
int main(void)
{
    int heure;
    scanf("%d", &heure);
    (heure > 8 && heure < 20) ? printf("Il fait jour.\n") : printf("Il fait nuit.\n");
    return 0;
}

Il est également possible de l’écrire sur plusieurs lignes, même si cette pratique est moins courante.

(heure > 8 && heure < 20)
    ? printf("Il fait jour.\n")
    : printf("Il fait nuit.\n");

Cet opérateur peut sembler inutile de prime abord, mais il s’avère être un allié de choix pour simplifier votre code quand celui-ci requiert la vérification de conditions simples.

Exercice

Pour bien comprendre cette nouvelle notion, nous allons faire un petit exercice. Imaginez que nous voulions faire un mini jeu vidéo dans lequel nous affichons le nombre de coups du joueur. Seulement voilà, vous êtes maniaques du français et vous ne supportez pas qu’il y ait un « s » en trop ou en moins. Essayez de réaliser un programme qui demande à l’utilisateur d’entrer un nombre de coups puis qui affiche celui-ci correctement accordé.

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
    int nb_coups;
    printf("Donnez le nombre de coups : ");
    scanf("%d", &nb_coups);
    printf("Vous gagnez en %d coup%c\n", nb_coups, (nb_coups > 1) ? 's' : ' ');
    return 0;
}

Ce programme utilise l’opérateur conditionnel pour condenser l’expression et aller plus vite dans l’écriture du code. Sans lui nous aurions dû écrire quelque chose comme ceci.

#include <stdio.h>
int main(void)
{
    int nb_coups;
    printf("Donnez le nombre de coups : ");
    scanf("%d", &nb_coups);
    if (nb_coups > 1)
        printf("Vous gagnez en %d coups\n", nb_coups);
    else
        printf("Vous gagnez en %d coup\n", nb_coups);
    return 0;
}

Ce chapitre a été important, il vous a permis d’utiliser les conditions, l’instruction if , l’instruction switch et l’opérateur conditionnel. Aussi, si vous n’avez pas très bien compris ou que vous n’avez pas tout retenu, nous vous conseillons de relire ce chapitre.

Le chapitre suivant sera l’occasion de mettre en œuvre ce que vous avez appris puisqu’il s’agira de votre premier TP.

En résumé
  1. L’instruction if permet de conditionner l’exécution d’une suite d’instructions ;
  2. Une clause else peut compléter une instruction if afin d’exécuter une autre suite d’instructions dans le cas où la condition est fausse ;
  3. L’instruction switch permet de comparer la valeur d’une expression à une liste de constantes entières ;
  4. Dans le cas où une comparaison est vraie, les instructions exécutées sont celles suivant un case et précédant une instruction break ;
  5. Plusieurs case peuvent précéder la même suite d’instructions ;
  6. L’opérateur conditionnel peut être utilisé pour simplifier l’écriture de certaines conditions.


TP : déterminer le jour de la semaine - Le langage C • Tutoriels • Zeste de Savoir

TP : déterminer le jour de la semaine

Objectif

Votre objectif est de parvenir à réaliser un programme qui, suivant une date fournie par l’utilisateur sous la forme « jj/mm/aaaa », donne le jour de la semaine correspondant. Autrement dit, voici ce que devrait donner l’exécution de ce programme.

Entrez une date : 11/2/2015
C'est un mercredi
Entrez une date : 13/7/1970
C'est un lundi

Première étape

Pour cette première étape, vous allez devoir réaliser un programme qui demande à l’utilisateur un jour du mois de janvier de l’an un et qui lui précise de quel jour de la semaine il s’agit, le tout à l’aide de la méthode présentée ci-dessous.

Entrez un jour : 27
C’est un jeudi

Déterminer le jour de la semaine

Pour déterminer le jour de la semaine correspondant à une date, vous allez devoir partir du premier janvier de l’an un (c’était un samedi) et calculer le nombre de jours qui sépare cette date de celle fournie par l’utilisateur. Une fois ce nombre obtenu, il nous est possible d’obtenir le jour de la semaine correspondant à l’aide de l’opérateur modulo.

En effet, comme vous le savez, les jours de la semaine suivent un cycle et se répètent tous les sept jours. Or, le reste de la division entière est justement un nombre cyclique allant de zéro jusqu’au diviseur diminué de un. Voici ce que donne le reste de la division entière des chiffres 1 à 9 par 3.

1 % 3 = 1
2 % 3 = 2
3 % 3 = 0
4 % 3 = 1
5 % 3 = 2
6 % 3 = 0
7 % 3 = 1
8 % 3 = 2
9 % 3 = 0

Comme vous le voyez, le reste de la division oscille toujours entre zéro et deux. Ainsi, si nous attribuons un chiffre de zéro à six à chaque jour de la semaine (par exemple zéro pour samedi et ainsi de suite pour les autres) nous pouvons déduire le jour de la semaine correspondant à un nombre de jours depuis le premier janvier de l’an un.

Prenons un exemple : l’utilisateur entre la date du vingt-sept janvier de l’an un. Il y a vingt-six jours qui le sépare du premier janvier. Le reste de la division entière de vingt-six par 7 est 5, il s’agit donc d’un jeudi.

Si vous le souhaitez, vous pouvez vous aider du calendrier suivant.

Afficher/Masquer le contenu masqué
     Janvier 1
di lu ma me je ve sa
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31

À présent, à vous de jouer. ;)

Correction

Alors, cela s’est bien passé ? Si oui, félicitations, si non, la correction devrait vous aider à y voir plus clair. ;)

Afficher/Masquer le contenu masqué <> # include <stdio.h> int main ( void ) { unsigned jour; printf ( "Entrez un jour : " ); scanf ( "%u" , &jour); switch ((jour - 1 ) % 7 ) { case 0 : printf ( "C'est un samedi\n" ); break ; case 1 : printf ( "C'est un dimanche\n" ); break ; case 2 : printf ( "C'est un lundi\n" ); break ; case 3 : printf ( "C'est un mardi\n" ); break ; case 4 : printf ( "C'est un mercredi\n" ); break ; case 5 : printf ( "C'est un jeudi\n" ); break ; case 6 : printf ( "C'est un vendredi\n" ); break ; } return 0 ; }

Tout d’abord, nous demandons à l’utilisateur d’entrer un jour du mois de janvier que nous affectons à la variable jour . Ensuite, nous calculons la différence de jours séparant le premier janvier de celui entré par l’utilisateur. Enfin, nous appliquons le modulo à ce résultat afin d’obtenir le jour de la semaine correspondant.

Deuxième étape

Bien, complexifions à présent un peu notre programme et demandons à l’utilisateur de nous fournir un jour et un mois de l’an un.

Entrez une date (jj/mm) : 20/4
C'est un mercredi

Pour ce faire, vous allez devoir convertir chaque mois en son nombre de jours et ajouter ensuite celui-ci au nombre de jours séparant la date entrée du premier du mois. À cette fin, vous pouvez considérer dans un premier temps que chaque mois compte trente et un jours et ensuite retrancher les jours que vous avez compté en trop suivant le mois fourni.

Par exemple, si l’utilisateur vous demande quel jour de la semaine était le vingt avril de l’an un :

  • vous multipliez trente et un par trois puisque trois mois séparent le mois d’avril du mois de janvier (janvier, février et mars) ;
  • vous retranchez trois jours (puisque le mois de février ne comporte que vingt-huit jours les années non bissextiles) ;
  • enfin, vous ajoutez les dix-neuf jours qui séparent la date fournie du premier du mois.

Au total, vous obtenez alors cent et neuf jours, ce qui nous donne, modulo sept, le nombre quatre, c’est donc un mercredi.

À toutes fins utiles, voici le calendrier complet de l’an un.

/Masquer le contenu masqué
      Janvier               Février                 Mars
di lu ma me je ve sa  di lu ma me je ve sa  di lu ma me je ve sa
                   1         1  2  3  4  5         1  2  3  4  5
 2  3  4  5  6  7  8   6  7  8  9 10 11 12   6  7  8  9 10 11 12
 9 10 11 12 13 14 15  13 14 15 16 17 18 19  13 14 15 16 17 18 19
16 17 18 19 20 21 22  20 21 22 23 24 25 26  20 21 22 23 24 25 26
23 24 25 26 27 28 29  27 28                 27 28 29 30 31
30 31
       Avril                  Mai                   Juin
di lu ma me je ve sa  di lu ma me je ve sa  di lu ma me je ve sa
                1  2   1  2  3  4  5  6  7            1  2  3  4
 3  4  5  6  7  8  9   8  9 10 11 12 13 14   5  6  7  8  9 10 11
10 11 12 13 14 15 16  15 16 17 18 19 20 21  12 13 14 15 16 17 18
17 18 19 20 21 22 23  22 23 24 25 26 27 28  19 20 21 22 23 24 25
24 25 26 27 28 29 30  29 30 31              26 27 28 29 30
      Juillet                 Août               Septembre
di lu ma me je ve sa  di lu ma me je ve sa  di lu ma me je ve sa
                1  2      1  2  3  4  5  6               1  2  3
 3  4  5  6  7  8  9   7  8  9 10 11 12 13   4  5  6  7  8  9 10
10 11 12 13 14 15 16  14 15 16 17 18 19 20  11 12 13 14 15 16 17
17 18 19 20 21 22 23  21 22 23 24 25 26 27  18 19 20 21 22 23 24
24 25 26 27 28 29 30  28 29 30 31           25 26 27 28 29 30
31
      Octobre               Novembre              Décembre
di lu ma me je ve sa  di lu ma me je ve sa  di lu ma me je ve sa
                   1         1  2  3  4  5               1  2  3
 2  3  4  5  6  7  8   6  7  8  9 10 11 12   4  5  6  7  8  9 10
 9 10 11 12 13 14 15  13 14 15 16 17 18 19  11 12 13 14 15 16 17
16 17 18 19 20 21 22  20 21 22 23 24 25 26  18 19 20 21 22 23 24
23 24 25 26 27 28 29  27 28 29 30           25 26 27 28 29 30 31
30 31

À vos claviers !

Correction

Bien, passons à la correction. :)

Afficher/Masquer le contenu masqué
#include <stdio.h>
int
main(void)
{
    unsigned jour;
    unsigned mois;
    printf("Entrez une date (jj/mm) : ");
    scanf("%u/%u", &jour, &mois);
    unsigned njours = (mois - 1) * 31;
    switch (mois)
    {
    case 12:
        --njours;
    case 11:
    case 10:
        --njours;
    case 9:
    case 8:
    case 7:
        --njours;
    case 6:
    case 5:
        --njours;
    case 4:
    case 3:
        njours -= 3;
        break;
    }
    njours += (jour - 1);
    switch (njours % 7)
    {
    case 0:
        printf("C'est un samedi\n");
        break;
    case 1:
        printf("C'est un dimanche\n");
        break;
    case 2:
        printf("C'est un lundi\n");
        break;
    case 3:
        printf("C'est un mardi\n");
        break;
    case 4:
        printf("C'est un mercredi\n");
        break;
    case 5:
        printf("C'est un jeudi\n");
        break;
    case 6:
        printf("C'est un vendredi\n");
        break;
    }
    return 0;
}

Nous commencons par demander deux nombres à l’utilisateur qui sont affectés aux variables jours et mois . Ensuite, nous multiplions le nombre de mois séparant celui entré par l’utilisateur du mois de janvier par trente et un. Après quoi, nous soustrayons les jours comptés en trop suivant le mois fourni. Enfin, nous ajoutons le nombre de jours séparant celui entré du premier du mois, comme pour la première étape.

Notez que nous avons utilisé ici une propriété intéressante de l’instruction switch : si la valeur de contrôle correspond à celle d’une entrée, alors les instructions sont exécutées jusqu’à rencontrer une instruction break (ou jusqu’à la fin du switch ). Ainsi, si le mois entré est celui de mai, l’instruction --njours va être exécutée, puis l’instruction njours -= 3 va également être exécutée.

Troisième et dernière étape

À présent, il est temps de réaliser un programme complet qui correspond aux objectifs du TP. Vous allez donc devoir demander à l’utilisateur une date entière et lui donner le jour de la semaine correspondant.

Entrez une date : 11/2/2015
C'est un mercredi

Toutefois, avant de vous lancer dans la réalisation de celui-ci, nous allons parler calendriers et années bissextiles. ;)

Les calendriers Julien et Grégorien

Vous le savez certainement, une année bissextile est une année qui comporte 366 jours au lieu de 365 et qui se voit ainsi ajouter un vingt-neuf février. Ce que vous ne savez en revanche peut-être pas, c’est que la détermination des années bissextile a varié au cours du temps.

Jusqu’en 1582, date d’adoption du calendrier Grégorien (celui qui est en vigueur un peu près partout actuellement), c’est le calendrier Julien qui était en application. Ce dernier considérait une année comme bissextile si celle-ci était multiple de quatre. Cette méthode serait correcte si une année comportait 365,25 jours. Cependant, il s’est avéré plus tard qu’une année comportait en fait 365,2422 jours.

Dès lors, un décalage par rapport au cycle terrestre s’était lentement installé ce qui posa problème à l’Église catholique pour le calcul de la date de Pâques qui glissait doucement vers l’été. Le calendrier Grégorien fût alors instauré en 1582 pour corriger cet écart en modifiant la règle de calcul des années bissextile : il s’agit d’une année multiple de quatre et , s’il s’agit d’une année multiple de 100, également multiple de 400. Par exemple, les années 1000 et 1100 ne sont plus bissextiles à l’inverse de l’année 1200 qui, elle, est divisible par 400.

Toutefois, ce ne sont pas douze années bissextiles qui ont été supprimées lors de l’adoption du calendrier Grégorien (100, 200, 300, 500, 600, 700, 900, 1000, 1100, 1300, 1400, 1500), mais seulement dix afin de rapprocher la date de Pâques de l’équinoxe de printemps.

Mode de calcul

Pour réaliser votre programme, vous devrez donc vérifier si la date demandée est antérieure ou postérieure à l’an 1582. Si elle est inférieure ou égale à l’an 1582, alors vous devrez appliquer le calendrier Julien. Si elle est supérieure, vous devrez utiliser le calendrier Grégorien.

Pour vous aider, voici un schéma que vous pouvez suivre.

Si l'année est supérieure à 1582
    Multiplier la différence d'années par 365
    Ajouter au résultat le nombre d'années multiples de 4
    Soustraire à cette valeur le nombre d'années multiples de 100
    Ajouter au résultat le nombre d'années multiples de 400
    Ajouter deux à ce nombre (du fait que seules dix années ont été supprimées en 1582)
Si l'année est inférieure ou égale à 1582
    Multiplier la différence d'années par 365
    Ajouter au résultat le nombre d'années multiples de 4
Au nombre de jours obtenus, ajouter la différence de jours entre
le mois de janvier et le mois fourni. N'oubliez pas que les mois comportent
trente et un ou trente jours et que le mois de février comporte pour sa
part vingt-huit jours sauf les années bisextiles où il s'en voit ajouter un
vingt-neuvième. Également, faites attention au calendrier en application pour
la détermination des années bissextiles !
Au résultat obtenu ajouter le nombre de jour qui sépare celui entré du premier
du mois.
Appliquer le modulo et déterminer le jour de la semaine.

À vous de jouer ! :)

Correction

Ça va, vous tenez bon ? :D

Afficher/Masquer le contenu masqué
#include <stdio.h>
int
main(void)
{
    unsigned jour;
    unsigned mois;
    unsigned an;
    printf("Entrez une date (jj/mm/aaaa) : ");
    scanf("%u/%u/%u", &jour, &mois, &an);
    unsigned njours = (an - 1) * 365;
    if (an > 1582) /* Calendrier Grégorien */
    {
        njours += ((an - 1) / 4);
        njours -= ((an - 1) / 100);
        njours += ((an - 1) / 400);
        njours += 2;
    }
    else /* Calendrier Julien */
        njours += ((an - 1) / 4);
    njours += (mois - 1) * 31;
    switch (mois)
    {
    case 12:
        --njours;
    case 11:
    case 10:
        --njours;
    case 9:
    case 8:
    case 7:
        --njours;
    case 6:
    case 5:
        --njours;
    case 4:
    case 3:
        if (an > 1582)
        {
            if (an % 4 == 0 && (an % 100 != 0 || an % 400 == 0))
                njours -= 2;
            else
                njours -= 3;
        }
        else
        {
            if (an % 4 == 0)
                njours -= 2;
            else
                njours -= 3;
        }
        break;
    }
    njours += (jour - 1);
    switch (njours % 7)
    {
    case 0:
        printf("C'est un samedi\n");
        break;
    case 1:
        printf("C'est un dimanche\n");
        break;
    case 2:
        printf("C'est un lundi\n");
        break;
    case 3:
        printf("C'est un mardi\n");
        break;
    case 4:
        printf("C'est un mercredi\n");
        break;
    case 5:
        printf("C'est un jeudi\n");
        break;
    case 6:
        printf("C'est un vendredi\n");
        break;
    }
    return 0;
}

Tout d’abord, nous demandons à l’utilisateur d’entrer une date au format jj/mm/aaaa et nous attribuons chaque partie aux variables jour , mois et an . Ensuite, nous multiplions par 365 la différence d’années séparant l’année fournie de l’an un. Toutefois, il nous faut encore prendre en compte les années bissextiles pour que le nombre de jours obtenus soit correct. Nous ajoutons donc un jour par année bissextile en prenant soin d’appliquer les règles du calendrier en vigueur à la date fournie.

Maintenant, il nous faut ajouter le nombre de jours séparant le mois de janvier du mois spécifié par l’utilisateur. Pour ce faire, nous utilisons la même méthode que celle vue lors de la deuxième étape à une différence près : nous vérifions si l’année courante est bissextile afin de retrancher le bon nombre de jours (le mois de février comportant dans ce cas vingt-neuf jours et non vingt-huit).

Enfin, nous utilisons le même code que celui de la première étape.

Ce chapitre nous aura permis de faire une petite pause et de mettre en application ce que nous avons vu dans les chapitres précédents. Reprenons à présent notre route en attaquant la notion de boucle.


Les boucles - Le langage C • Tutoriels • Zeste de Savoir

Les boucles

La boucle while

La première des boucles que nous allons étudier est la boucle while (qui signifie « tant que »). Celle-ci permet de répéter un bloc d’instructions tant qu’une condition est remplie.

+------------>|
|             |
|       +-----v-----+   Faux
|       | Condition |-------------------+
|       +-----------+                   |
|             |                         |
|        Vrai |                         |
|             |                         |
|   +---------v-----------+   +---------v----------+
|   | Bloc d'instructions |   | Suite du programme |
|   | à répéter           |   +--------------------+
|   +---------------------+
|             |
+-------------+

Syntaxe

La syntaxe de notre boucle while est assez simple.

while (/* Condition */)
{
    /* Bloc d'instructions à répéter */
}

Si vous n’avez qu’une seule instruction à réaliser, vous avez la possibilité de ne pas mettre d’accolades.

while (/* Condition */)
    /* Une seule instruction */

Exemple

#include <stdio.h>
int main(void)
{
    int i = 0;
    while (i < 5)
    {
        printf("La variable i vaut %d\n", i);
        i++;
    }
    return 0;
}
Résultat
La variable i vaut 0
La variable i vaut 1
La variable i vaut 2
La variable i vaut 3
La variable i vaut 4

Le fonctionnement est simple à comprendre :

  • Au départ, notre variable i vaut zéro. Étant donné que zéro est bien inférieur à cinq, la condition est vraie, le corps de la boucle est donc exécuté.
  • La valeur de i est affichée.
  • i est augmentée d’une unité et vaut désormais un.
  • La condition de la boucle est de nouveau vérifiée.

Ces étapes vont ainsi se répéter pour les valeurs un, deux, trois et quatre. Quand la variable i vaudra cinq, la condition sera fausse, et l’instruction while sera alors passée.

Dans cet exemple, nous utilisons une variable nommée i . Ce nom lui vient d’une contraction du mot anglais iterator qui signifie que cette variable sert à l’itération (la répétition) du corps de la boucle. Ce nom est tellement court et explicite qu’il est pour ainsi dire devenu une convention de nommage en C.

Exercice

Essayez de réaliser un programme qui détermine si un nombre entré par l’utilisateur est premier. Pour rappel, un nombre est dit premier s’il n’est divisible que par un et par lui-même. Notez que si un nombre x x x x est divisible par y y y y alors le résultat de l’opération x % y est nul.

Indice

Afficher/Masquer le contenu masqué

Pour savoir si un nombre est premier, il va vous falloir vérifier si celui-ci est uniquement divisible par un et lui-même. Dit autrement, vous allez devoir contrôler qu’aucun nombre compris entre 1 et le nombre entré (tout deux exclus) n’est un diviseur de ce dernier. Pour parcourir ces différentes possibilités, une boucle va vous être nécessaire.

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
   int nombre;
   printf("Entrez un nombre : ");
   scanf("%d", &nombre);
   int i = 2;
   while ((i < nombre) && (nombre % i != 0))
   {
       ++i;
   }
   if (i == nombre)
   {
       printf("%d est un nombre premier\n", nombre);
   }
   else
   {
       printf("%d n'est pas un nombre premier\n", nombre);
   }
   return 0;
}

La boucle do-while

La boucle do while fonctionne comme la boucle while , à un petit détail près : elle s’exécutera toujours au moins une fois, alors qu’une boucle while peut ne pas s’exécuter si la condition est fausse dès le départ.

+------------>|
|             |
|   +---------v-----------+
|   | Bloc d'instructions |
|   | à répéter           |
|   +---------------------+
|             |
|             |
|             |
|       +-----v-----+    Faux
|       | Condition +-------------+
|       +-----------+             |
|             |                   |
+-------------+                   |
       Vrai                       |
                        +---------v----------+
                        | Suite du programme |
                        +--------------------+

Syntaxe

À la différence de la boucle while , la condition est placée à la fin du bloc d’instruction à répéter, ce qui explique pourquoi celui-ci est toujours exécuté au moins une fois. Remarquez également la présence d’un point-virgule à la fin de l’instruction qui est obligatoire.

do
{
    /* Bloc d'instructions à répéter */
} while (/* Condition */);

Si vous n’avez qu’une seule instruction à exécuter, les accolades sont facultatives.

do
    /* Une seule instruction */
while (/* Condition */);

Exemple 1

Voici le même code que celui présenté avec l’instruction while .

#include <stdio.h>
int main(void)
{
    int i = 0;
    do
    {
        printf("La variable i vaut %d\n", i);
        ++i;
    } while (i < 5);
    return 0;
}
Résultat
La variable i vaut 0
La variable i vaut 1
La variable i vaut 2
La variable i vaut 3
La variable i vaut 4

Exemple 2

Comme nous vous l’avons dit plus haut, une boucle do while s’éxecute au moins une fois.

#include <stdio.h>
int main(void)
{
    do
        printf("Boucle do-while\n");
    while (0);
    return 0;
}

Boucle do-while

Comme vous le voyez, bien que la condition est fausse (pour rappel, une valeur nulle correspond à une valeur fausse), le corps de la boucle est exécuté une fois puisque la condition n’est évaluée qu’ après le parcours du bloc d’instructions.

La boucle for

Syntaxe

for (/* Expression/Déclaration */; /* Condition */; /* Expression */)
{
    /* Instructions à répéter */
}

Une boucle for se décompose en trois parties (ou trois clauses) :

  • une expression et/ou une déclaration qui sera le plus souvent l’initialisation d’une variable ;
  • une condition ;
  • une seconde expression, qui consistera le plus souvent en l’incrémentation d’une variable.

Techniquement, une boucle for revient en fait à écrire ceci.

/* Expression/Déclaration */
while (/* Condition */)
{
    /* Bloc d'instructions à répéter */
    /* Expression */
}

Comme pour les deux autres boucles, si le corps ne comporte qu’une seule instruction, les accolades ne sont pas nécessaires.

for (/* Expression/Déclaration */; /* Condition */; /* Expression */)
    /* Une seule instruction */

Exemple

Le fonctionnement de cette boucle est plus simple à appréhender à l’aide d’un exemple.

#include <stdio.h>
int main(void)
{
    for (int i = 0; i < 5; ++i)
        printf("la variable i vaut %d\n", i);
    return 0;
}
Résultat
variable vaut 0
variable vaut 1
variable vaut 2
variable vaut 3
variable vaut 4

Ce qui, comme dit précédemment, revient exactement à écrire cela.

#include <stdio.h>
int main(void)
{
    int i = 0;
    while (i < 5)
    {
        printf("la variable i vaut %d\n", i);
        ++i;
    }
    return 0;
}

Notez bien que la déclaration int i = 0 est située en dehors du corps de la boucle. Elle n’est donc pas exécutée à chaque tour.

Si vous déclarez une variable au sein de l’instruction for , celle-ci ne sera utilisable qu’au sein de cette boucle . Le code suivant est donc incorrect.

#include <stdio.h>
int main(void)
{
    for (int i = 0; i < 5; ++i)
        printf("la variable i vaut %d\n", i);
    printf("i = %d\n", i); /* Faux, i n'est pas utilisable hors de la boucle for */
    return 0;
}

Dans un tel cas, il est nécessaire de déclarer la variable avant et en dehors de la boucle for .

#include <stdio.h>
int main(void)
{
    int i;
    for (i = 0; i < 5; ++i)
        printf("la variable i vaut %d\n", i);
    printf("i = %d\n", i); /* Ok */
    return 0;
}

Exercice

Essayez de réaliser un programme qui calcule la somme de tous les nombres compris entre un et n n n n ( n n n n étant déterminé par vos soins). Autrement dit, pour un nombre n n n n donné, vous allez devoir calculer 1 + 2 + 3 + . . . + ( n − 2 ) + ( n − 1 ) + n 1 + 2 + 3 + . . . + ( n − 2 ) + ( n − 1 ) + n 1 + 2 + 3 + ... + (n-2) + (n-1) + n 1 + 2 + 3 + . . . + ( n − 2 ) + ( n − 1 ) + n .

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main (void)
{
    const unsigned int n = 250;
    unsigned somme = 0;
    for (unsigned i = 1; i <= n; ++i)
        somme += i;
    printf ("Somme de 1 à %u : %u\n", n, somme);
    return 0;
}

Notez qu’il est possible de réaliser cet exercice sans boucle en calculant : N × ( N + 1 ) 2 N × ( N + 1 ) 2 \frac {N \times (N+1)} {2} 2 N × ( N + 1 ) ​ .

Plusieurs compteurs

Notez que le nombre de compteurs, de déclarations ou de conditions n’est pas limité, comme le démontre le code suivant.

for (int i = 0, j = 2; i < 10 && j < 12; i++, j += 2)

Ici, nous définissons deux compteurs i et j initialisés respectivement à zéro et deux. Le contenu de la boucle est exécuté tant que i est inférieur à dix et que j est inférieur à douze, i étant augmentée de une unité et j de deux unités à chaque tour de boucle. Le code est encore assez lisible, cependant la modération est de mise, un trop grand nombre de paramètres rendant la boucle for illisible.

Imbrications

Il est parfaitement possible d’imbriquer une ou plusieurs boucles en plaçant une boucle dans le corps d’une autre boucle.

for (int i = 0; i < 1000; ++i)
{
    for (int j = i; j < 1000; ++j)
    {
          /*  Code  */
    }
}

Cela peut servir par exemple pour déterminer la liste des nombres dont le produit vaut mille.

#include <stdio.h>
int main(void)
{
    for (int i = 0; i <= 1000; ++i)
        for (int j = i; j <= 1000; ++j)
            if (i * j == 1000)
                printf ("%d * %d = 1000 \n", i, j);
    return 0;
}
Résultat
1 * 1000 = 1000
2 * 500 = 1000
4 * 250 = 1000
5 * 200 = 1000
8 * 125 = 1000
10 * 100 = 1000
20 * 50 = 1000
25 * 40 = 1000

Vous n’êtes bien entendu pas tenu d’imbriquer des types de boucles identiques. Vous pouvez parfaitement plaçer, par exemple, une boucle while dans une boucle for .

Boucles infinies

Lorsque vous utilisez une boucle, il y a une chose que vous devez impérativement vérifier : elle doit pouvoir se terminer. Cela paraît évident de prime abord, pourtant il s’agit d’une erreur de programmation assez fréquente qui donne lieu à des boucles infinies. Soyez donc vigilants !

L’exemple le plus fréquent est l’oubli d’incrémentation de l’itérateur.

#include <stdio.h>
int main(void)
{
    int i = 0;
    while (i < 5)
    {
        printf("La variable i vaut %d\n", i);
        /* Oubli d'incrémentation. */
    }
    return 0;
}
Résultat
La variable i vaut 0
La variable i vaut 0
La variable i vaut 0
...

Ce programme continuera son exécution jusqu’à ce qu’il soit arrêté.

Pour stopper l’exécution d’un programme, vous pouvez utiliser la combinaison Ctrl + c ou simplement fermer votre terminal.

Exercices

Calcul du PGCD de deux nombres

Le PGCD de deux nombres est le plus grand nombre qui peut diviser ces derniers. Par exemple, le PGCD de quinze et douze est trois et celui de vingt-quatre et dix-huit est six.

Pour le calculer, nous devons disposer de deux nombres a a a a et b b b b avec a a a a supérieur à b b b b . Ensuite, nous effectuons la division entière de a a a a par b b b b .

  • si le reste est nul, alors nous avons terminé ;
  • si le reste est non nul, nous revenons au début en remplaçant a a a a par b b b b et b b b b par le reste.

Avec cette explication, vous avez tout ce qu’il vous faut : à vos claviers !

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main (void)
{
    unsigned int a = 46;
    unsigned int b = 42;
    unsigned int reste = a % b;
    while (reste != 0)
    {
        a = b;
        b = reste;
        reste = a % b;
    }
    printf("%d", b);
    return 0;
}

Une overdose de lapins

Au treizième siècle, un mathématicien italien du nom de Leonardo Fibonacci posa un petit problème dans un de ses livres, qui mettait en scène des lapins. Ce petit problème mit en avant une suite de nombres particulière, nommée la suite de Fibonnaci, du nom de son inventeur. Il fit les hypothèses suivantes :

  • le premier mois, nous plaçons un couple de deux lapins dans un enclos ;
  • un couple de lapin ne peut procréer qu’à partir du troisième mois de sa venue dans l’enclos (autrement dit, il ne se passe rien pendant les deux premiers mois) ;
  • chaque couple capable de procréer donne naissance à un nouveau couple ;
  • enfin, pour éviter tout problème avec la SPA, les lapins ne meurent jamais.

Le problème est le suivant : combien y a-t-il de couples de lapins dans l’enclos au n-ième mois ? Le but de cet exercice est de réaliser un petit programme qui fasse ce calcul automatiquement.

Indice

Afficher/Masquer le contenu masqué

Allez, un petit coup de pouce : suivant l’énoncé, un couple ne donne naissance à un autre couple qu’au début du troisième mois de son apparition. Combien de couple y a-t-il le premier mois ? Un seul. Combien y en a-t-il le deuxième mois ? Toujours un seul. Combien y en a-t-il le troisième mois (le premier couple étant là depuis deux mois) ? Deux. Avec ceci, vous devriez venir à bout du problème.

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
    int a = 0;
    int b = 1;
    int nb_lapins = 1;
    int const mois = 10;
    for (int i = 1; i < mois; ++i)
    {
        nb_lapins = a + b;
        a = b;
        b = nb_lapins;
    }
    printf("Au mois %d, il y a %d lapins\n", mois, nb_lapins);
    return 0;
}

Des pieds et des mains pour convertir mille miles

Si vous avez déjà voyagé en Grande-Bretagne ou aux États-unis, vous savez que les unités de mesure utilisées dans ces pays sont différentes des nôtres. Au lieu de notre cher système métrique, dont les stars sont les centimètres, mètres et kilomètres, nos amis outre-manche et outre-atlantique utilisent le système impérial, avec ses pouces, pieds et miles , voire lieues et furlongs ! Et pour empirer les choses, la conversion n’est pas toujours simple à effectuer de tête… Aussi, la lecture d’un ouvrage tel que Le Seigneur des Anneaux , dans lequel toutes les distances sont exprimées en unités impériales, peut se révéler pénible.

Grâce au langage C, nous allons aujourd’hui résoudre tous ces problèmes ! Votre mission, si vous l’acceptez, sera d’écrire un programme affichant un tableau de conversion entre miles et kilomètres. Le programme ne demande rien à l’utilisateur, mais doit afficher quelque chose comme ceci.

Miles    Km
5        8
10       16
15       24
20       32
25       40
30       48

Autrement dit, le programme compte les kilomètres de cinq en cinq jusqu’à trente et affiche à chaque fois la valeur correspondante en miles . Un mile vaut exactement 1.609344 km, cependant nous allons utiliser une valeur approchée : huit-cinquièmes de kilomètre (soit 1.6km). Autrement dit, 1 1 1 1 miles = 8 5 = 8 5 = \frac{8}{5} = 5 8 ​ km ou ( 1 1 1 1 km = 5 8 = 5 8 = \frac{5}{8} = 8 5 ​ miles ).

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
    unsigned miles = 0;
    printf("Miles\tKm\n");
    do
    {
        ++miles;
        printf("%u\t%u\n", miles * 5, miles * 8);
    } while (miles * 5 < 30);
    return 0;
}

Puissances de trois

Passons à un exercice un peu plus difficile, du domaine des mathématiques. Essayez de le faire même si vous n’aimez pas les mathématiques.

Vous devez vérifier si un nombre est une puissance de trois, et afficher le résultat. De plus, si c’est le cas, vous devez afficher l’exposant qui va avec.

Indice

Afficher/Masquer le contenu masqué

Pour savoir si un nombre est une puissance de trois, vous pouvez utiliser le modulo. Attention cependant : si le reste vaut 0, le nombre n’est pas forcément une puissance de trois (par exemple, le reste de la division de 15 par 3 est nul, mais 15 n’est pas une puissance de trois).

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
/* Un petite explication s'impose, notamment au niveau du for. La première
partie qui correspond à l'initialisation de i ne devrait pas vous poser
trop de soucis. Ensuite, le i /= 3 sert à diviser i par trois à chaque
itération. Au tour de la condition, le principe est simple : tant que le
reste de la division de i par 3 est égal à zéro et que i est positif, on
incrémente l'exposant. Enfin, pour déterminer si le nombre est une puissance
de trois, il suffit de vérifier si i est égal à 1 (essayez avec de petits
nombres dans votre tête ou sur papier si vous n'êtes pas convaincu). */
int main(void)
{
    int number;
    printf("Veuillez entrez un nombre : ");
    scanf("%d", &number);
    int exposant = 0;
    for (int i = number; (i % 3 == 0) && (i > 0); i /= 3)
        ++exposant;
    /* Chaque division successive divise i par 3, donc si nous obtenons finalement
    i == 1, c'est que le nombre est bien une puissance de 3 */
    if (i == 1)
        printf ("%d est égal à 3 ^ %d\n", number, exposant);
    else
        printf("%d n'est pas une puissance de 3\n", number);
    return 0;
}

La disparition : le retour

Connaissez-vous le roman La Disparition ? Il s’agit d’un roman français de Georges Perec, publié en 1969. Sa particularité est qu’il ne contient pas une seule fois la lettre « e ». On appelle ce genre de textes privés d’une lettre des lipogrammes . Celui-ci est une prouesse littéraire, car la lettre « e » est la plus fréquente de la langue française : elle représente une lettre sur six en moyenne ! Le roman faisant environ trois cents pages, il a sûrement fallu déployer des trésors d’inventivité pour éviter tous les mots contenant un « e ».

Si vous essayez de composer un tel texte, vous allez vite vous rendre compte que vous glissez souvent des « e » dans vos phrases sans même vous en apercevoir. Nous avons besoin d’un vérificateur qui nous sermonnera chaque fois que nous écrirons un « e ». C’est là que le langage C entre en scène !

Écrivez un programme qui demande à l’utilisateur de taper une phrase, puis qui affiche le nombre de « e » qu’il y a dans celle-ci. Une phrase se termine toujours par un point « . », un point d’exclamation « ! » ou un point d’interrogation « ? ». Pour effectuer cet exercice, il sera indispensable de lire la phrase caractère par caractère.

Entrez une phrase : Bonjour, comment allez-vous ?
Au moins une lettre 'e' a été repérée (précisémment : 2) !

Indice

Afficher/Masquer le contenu masqué

La première chose à faire est d’afficher un message de bienvenue, afin que l’utilisateur sache quel est votre programme. Ensuite, Il vous faudra lire les caractères tapés (rappelez-vous les différents formats de la fonction scanf() ), un par un, jusqu’à ce qu ’un point (normal, d’exclamation ou d’interrogation) soit rencontré. Dans l’intervalle, il faudra compter chaque « e » qui apparaîtra. Enfin, il faudra afficher le nombre de « e » qui ont été comptés (potentiellement aucun).

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
    printf("Entrez une phrase se terminant par '.', '!' ou '?' : ");
    unsigned compteur = 0;
    char c;
    do
    {
        scanf("%c", &c);
        if (c == 'e' || c == 'E')
            compteur++;
    } while (c != '.' && c != '!' && c != '?');
    if (compteur == 0)
        printf("Aucune lettre 'e' repérée. Félicitations !\n");
    else
        printf("Au moins une lettre 'e' a été repérée (précisémment : %d) !\n", compteur);
    return 0;
}

Les boucles sont assez faciles à comprendre, la seule chose dont il faut se souvenir étant de faire attention de bien avoir une condition de sortie pour ne pas tomber dans une boucle infinie. Le prochain chapitre abordera la notion de saut.

En résumé
  1. Une boucle permet de répéter l’exécution d’une suite d’instructions tant qu’une condition est vraie ;
  2. La boucle while évalue une condition avant d’exécuter une suite d’instructions ;
  3. La boucle do while évalue une condition après avoir exécuté une suite d’instructions ;
  4. La boucle for est une forme condensée de la boucle while ;
  5. Une variable définie dans la première clause d’une boucle for ne peut être utilisée qu’au sein de cette boucle ;
  6. Une boucle dont la condition est toujours vraie est appelée une boucle infinie.


Les sauts - Le langage C • Tutoriels • Zeste de Savoir

Les sauts

L'instruction break

Nous avons déjà rencontré l’instruction break lors de la présentation de l’instruction switch , cette dernière permettait de quitter le bloc d’un switch pour reprendre immédiatement après. Cependant, l’instruction break peut également être utilisée au sein d’une boucle pour stopper son exécution (autrement dit pour effectuer un saut au-delà du bloc à répéter).

Exemple

Le plus souvent, une instruction break est employée pour sortir d’une itération lorsqu’une condition (différente de celle contrôlant l’exécution de la boucle) est remplie. Par exemple, si nous souhaitons réaliser un programme qui détermine le plus petit diviseur commun de deux nombres, nous pouvons utiliser cette instruction comme suit.

#include <stdio.h>
int main(void)
{
    int a;
    int b;
    printf("Entrez deux nombres : ");
    scanf("%d %d", &a, &b);
    int min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
        {
            printf("le plus petit diviseur de %d et %d est %d\n", a, b, i);
            break;
        }
    return 0;
}
Résultat
Entrez deux nombres : 112 567
le plus petit diviseur de 112 et 567 est 7
Entrez deux nombres : 13 17

Comme vous le voyez, la condition principale permet de progresser parmi les diviseurs possibles alors que la seconde détermine si la valeur courante de i est un diviseur commun. Si c’est le cas, l’exécution de la boucle est stoppée et le résultat affiché. Dans le cas où il n’y a aucun diviseur commun, la boucle s’arrête lorsque le plus petit des deux nombres est atteint.

L’instruction continue

L’instruction continue permet d’arrêter l’exécution de l’itération courante. Autrement dit, celle-ci vous permet de retourner (sauter) directement à l’évaluation de la condition.

Exemple

Afin d’améliorer un peu l’exemple précédent, nous pourrions passer les cas où le diviseur testé est un multiple de deux (puisque si un des deux nombres n’est pas divisible par deux, il ne peut pas l’être par quatre, par exemple).

Ceci peut s’exprimer à l’aide de l’instruction continue .

#include <stdio.h>
int main(void)
{
    int a;
    int b;
    printf("Entrez deux nombres : ");
    scanf("%d %d", &a, &b);
    int min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
    {
        if (i != 2 && i % 2 == 0)
        {
            printf("je passe %d\n", i);
            continue;
        }
        if (a % i == 0 && b % i == 0)
        {
            printf("le plus petit diviseur de %d et %d est %d\n", a, b, i);
            break;
        }
    }
    return 0;
}
Résultat
je passe 4
je passe 6
le plus petit diviseur de 112 et 567 est 7

Dans le cas de la boucle for , l’exécution reprend à l’évaluation de sa deuxième expression (ici ++i ) et non à l’évaluation de la condition (qui a lieu juste après). Il serait en effet mal venu que la variable i ne soit pas incrémentée lors de l’utilisation de l’instruction continue .

Boucles imbriquées

Notez bien que les instructions break et continue n’affecte que l’exécution de la boucle dans laquelle elles sont situées. Ainsi, si vous utilisez l’instruction break dans une boucle imbriquée dans une autre, vous sortirez de la première, mais pas de la seconde.

#include <stdio.h>
int main(void)
{
    for (int i = 0 ; i <= 1000 ; ++i)
        for (int j = i ; j <= 1000 ; ++j)
            if (i * j == 1000)
            {
                printf ("%d * %d = 1000 \n", i, j);
                break; /* Quitte la boucle courante, mais pas la première. */
            }
    return 0;
}
Résultat
1 * 1000 = 1000
2 * 500 = 1000
4 * 250 = 1000
5 * 200 = 1000
8 * 125 = 1000
10 * 100 = 1000
20 * 50 = 1000
25 * 40 = 1000

L'instruction goto

Nous venons de voir qu’il était possible de réaliser des sauts à l’aide des instructions break et continue . Cependant, d’une part ces instructions sont confinées à une boucle ou à une instruction switch et, d’autre part, la destination du saut nous est imposée (la condition avec continue , la fin du bloc d’instructions avec break ).

L’instruction goto permet de sauter à un point précis du programme que nous aurons déterminé à l’avance. Pour ce faire, le langage C nous permet de marquer des instructions à l’aide d’étiquettes ( labels en anglais). Une étiquette n’est rien d’autre qu’un nom choisis par nos soins suivi du catactère : . Généralement, par souci de lisibilité, les étiquettes sont placées en retrait des instructions qu’elles désignent.

Exemple

Reprenons (encore) l’exemple du calcul du plus petit commun diviseur. Ce dernier aurait pu être écrit comme suit à l’aide d’une instruction goto .

#include <stdio.h>
int main(void)
{
    int a;
    int b;
    printf("Entrez deux nombres : ");
    scanf("%d %d", &a, &b);
    int min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
            goto trouve;
    return 0;
trouve:
    printf("le plus petit diviseur de %d et %d est %d\n", a, b, i);
    return 0;
}

Comme vous le voyez, l’appel à la fonction printf() a été marqué avec une étiquette nommée trouve . Celle-ci est utilisée avec l’instruction goto pour spécifier que c’est à cet endroit que nous souhaitons nous rendre si un diviseur commun est trouvé. Vous remarquerez également que nous avons désormais deux instructions return , la première étant exécutée dans le cas où aucun diviseur commun n’est trouvé.

Le dessous des boucles

Maintenant que vous savez cela, vous devriez être capable de réécrire n’importe quelle boucle à l’aide de cette instruction. En effet, une boucle se compose de deux sauts : un vers une condition et l’autre vers l’instruction qui suit le corps de la boucle. Ainsi, les deux codes suivants sont équivalents.

#include <stdio.h>
int main(void)
{
    int i = 0;
    while (i < 5)
    {
        printf("La variable i vaut %d\n", i);
        i++;
    }
    return 0;
}

#include <stdio.h>
int main(void)
{
    int i = 0;
condition:
    if (i < 5)
    {
        printf("La variable i vaut %d\n", i);
        i++;
        goto condition;
    }
    return 0;
}

Goto Hell ?

Bien qu’utile dans certaines circonstances, sachez que l’instruction goto est fortement décriée, principalement pour deux raisons :

  • mis à part dans des cas spécifiques, il est possible de réaliser la même action de manière plus claire à l’aide de structures de contrôles ;
  • l’utilisation de cette instruction peut amener votre code à être plus difficilement lisible et, dans les pires cas, en faire un code spaghetti.

À vrai dire, elle est aujourd’hui surtout utilisée dans le cas de la gestion d’erreur, ce que nous verrons plus tard dans ce cours. Aussi, en attendant, nous vous déconseillons de l’utiliser.

Dans le chapitre suivant, nous aborderons la notion de fonction.

En résumé
  1. Une instruction de saut permet de passer directement à l’exécution d’une instruction précise ;
  2. L’instruction break permet de sortir de la boucle ou de l’instruction switch dans laquelle elle est employée ;
  3. L’instruction continue permet de retourner à l’évaluation de la condition de la boucle dans laquelle elle est utilisée ;
  4. L’instruction goto permet de passer à l’exécution d’une instruction désignée par une étiquette.


Les fonctions - Le langage C • Tutoriels • Zeste de Savoir

Les fonctions

Qu'est-ce qu'une fonction ?

Le concept de fonction ne vous est pas inconnu : printf() , scanf() , et main() sont des fonctions.

Mais qu’est-ce qu’une fonction exactement et quel est leur rôle exactement ?

Une fonction est :

  • une suite d’instructions ;
  • marquée à l’aide d’un nom (comme une variable finalement) ;
  • qui a vocation à être exécutée à plusieurs reprises ;
  • qui rassemble des instructions qui permettent d’effectuer une tâche précise (comme afficher du texte à l’écran, calculer la racine carrée d’un nombre, etc).

Pour mieux saisir leur intérêt, prenons un exemple concret.

#include <stdio.h>
int main(void)
{
    int a;
    int b;
    printf("Entrez deux nombres : ");
    scanf("%d %d", &a, &b);
    int min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
        {
            printf("Le plus petit diviseur de %d et %d est %d\n", a, b, i);
            break;
        }
    return 0;
}

Ce code, repris du chapitre précédent, permet de calculer le plus petit commun diviseur de deux nombres donnés. Imaginons à présent que nous souhaitions faire la même chose, mais avec deux paires de nombres. Le code ressemblerait alors à ceci.

#include <stdio.h>
int main(void)
{
    int a;
    int b;
    printf("Entrez deux nombres : ");
    scanf("%d %d", &a, &b);
    int min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
        {
            printf("Le plus petit diviseur de %d et %d est %d\n", a, b, i);
            break;
        }
    printf("Entrez deux autres nombres : ");
    scanf("%d %d", &a, &b);
    min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
        {
            printf("Le plus petit diviseur de %d et %d est %d\n", a, b, i);
            break;
        }
    return 0;
}

Comme vous le voyez, ce n’est pas très pratique : nous devons recopier les instructions de calcul deux fois, ce qui est assez dommage et qui plus est source d’erreurs. C’est ici que les fonctions entre en jeu en nous permettant par exemple de rassembler les instructions dédiées au calcul du plus petit diviseur commun en un seul point que nous solliciterons autant de fois que nécessaire.

Oui, il est aussi possible d’utiliser une boucle pour éviter la répétition, mais l’exemple aurait été moins parlant. :-°

Définir et utiliser une fonction

Pour définir une fonction, nous allons devoir donner quatre informations sur celle-ci :

  • son nom : les règles sont les mêmes que pour les variables ;
  • son corps (son contenu) : le bloc d’instructions à exécuter ;
  • son type de retour : le type du résultat de la fonction ;
  • d’éventuels paramètres : des valeurs reçues par la fonction lors de l’appel.

La syntaxe est la suivante.

type nom(paramètres)
{
     /* Corps de la fonction */
}

Prenons un exemple en créant une fonction qui affiche « bonjour ! » à l’écran.

#include <stdio.h>
void bonjour(void)
{
    printf("Bonjour !\n");
}
int main(void)
{
    bonjour();
    return 0;
}

Comme vous le voyez, la fonction se nomme « bonjour » et est composée d’un appel à printf() . Reste les deux mots-clés void :

  • dans le cas du type de retour, il spécifie que la fonction ne retourne rien ;
  • dans le cas des paramètres, il spécifie que la fonction n’en reçoit aucun (cela se manifeste lors de l’appel : il n’y a rien entre les parenthèses).

Le type de retour

Le type de retour permet d’indiquer deux choses : si la fonction retourne une valeur et le type de cette valeur.

#include <stdio.h>
int deux(void)
{
    return 2;
}
int main(void)
{
    printf("Retour : %d\n", deux());
    return 0;
}
Résultat
Retour : 2

Dans l’exemple ci-dessus, la fonction deux() est définie comme retournant une valeur de type int . Vous retrouvez l’instruction return , une instruction de saut (comme break , continue et goto ). Ce return arrête l’exécution de la fonction courante et provoque un retour (techniquement, un saut) vers l’appel à cette fonction qui se voit alors attribuer la valeur de retour (s’il y en a une). Autrement dit, dans notre exemple, l’instruction return 2 stoppe l’exécution de la fonction deux() et ramène l’exécution du programme à l’appel qui vaut désormais 2, ce qui donne finalement printf("Retour : %d\n", 2)

Les paramètres

Un paramètre sert à fournir des informations à la fonction lors de son exécution. La fonction printf() par exemple récupère ce qu’elle doit afficher dans la console à l’aide de paramètres. Ceux-ci sont définis de la même manière que les variables si ce n’est que les définitions sont séparées par des virgules.

type nom(type paramètres1, type paramètres2, ...)
{
    /* Corps de la fonction */
}

Vous pouvez utiliser un maximum de 127 paramètres 1 (si, si :-° ), toutefois nous vous conseillons de vous limiter à cinq afin de conserver un code concis et lisible.

Maintenant que nous savons tout cela, nous pouvons réaliser une fonction qui calcule le plus petit commun diviseur entre deux nombres et ainsi simplifier l’exemple du dessus.

#include <stdio.h>
int ppcd(int a, int b)
{
    int min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
            return i;
    return 0;
}
int main(void)
{
    int a;
    int b;
    printf("Entrez deux nombres : ");
    scanf("%d %d", &a, &b);
    int resultat = ppcd(a, b);
    if (resultat != 0)
        printf("Le plus petit diviseur de %d et %d est %d\n", a, b, resultat);
    printf("Entrez deux autres nombres : ");
    scanf("%d %d", &a, &b);
    resultat = ppcd(a, b);
    if (resultat != 0)
        printf("Le plus petit diviseur de %d et %d est %d\n", a, b, resultat);
    return 0;
}

Plus simple et plus lisible, non ?

Remarquez la présence de deux instructions return dans la fonction ppcd() . La valeur zéro est retournée afin d’indiquer l’absence d’un diviseur commun.

Les arguments et les paramètres

À ce stade, il est important de préciser qu’un paramètre est propre à une fonction, il n’est pas utilisable en dehors de celle-ci. Par exemple, la variable a de la fonction ppcd() n’a aucun rapport avec la variable a de la fonction main() .

Voici un autre exemple plus explicite à ce sujet.

#include <stdio.h>
void fonction(int nombre)
{
    ++nombre;
    printf("Variable nombre dans `fonction' : %d\n", nombre);
}
int main(void)
{
    int nombre = 5;
    fonction(nombre);
    printf("Variable nombre dans `main' : %d\n", nombre);
    return 0;
}
Résultat
Variable nombre dans `fonction' : 6
Variable nombre dans `main' : 5

Comme vous le voyez, les deux variables nombre sont bel et bien distinctes. En fait, lors d’un appel de fonction, vous spécifiez des arguments à la fonction appelée. Ces arguments ne sont rien d’autres que des expressions dont les résultats seront ensuite affectés aux différents paramètres de la fonction.

Notez bien cette différence car elle est très importante : un argument est une expression alors qu’un paramètre est une variable .

Ainsi, la valeur de la variable nombre de la fonction main() est passée en argument à la fonction fonction() et est ensuite affectée au paramètre nombre . La variable nombre de la fonction main() n’est donc en rien modifiée.

  1. ISO/IEC 9899:201x, doc. N1570, § 5.2.4.1, Translation limits, p. 26

    ↩

Les prototypes

Jusqu’à présent, nous avons toujours défini notre fonction avant la fonction main() . Cela paraît de prime abord logique (nous définissons la fonction avant de l’utiliser), cependant cela est surtout indispensable. En effet, si nous déplaçons la définition après la fonction main() , le compilateur se retrouve dans une situation délicate : il est face à un appel de fonction dont il ne sait rien (nombres d’arguments, type des arguments et type de retour). Que faire ? Hé bien, il serait possible de stopper la compilation, mais ce n’est pas ce qui a été retenu, le compilateur va considérer que la fonction retourne une valeur de type int et qu’elle reçoit un nombre indéterminé d’arguments.

Toutefois, si cette décision à l’avantage d’éviter un arrêt de la compilation, elle peut en revanche conduire à des problèmes lors de l’exécution si cette supposition du compilateur s’avère inadéquate. Or, il serait pratique de pouvoir définir les fonctions dans l’ordre que nous souhaitons sans se soucier de qui doit être défini avant qui.

Pour résoudre ce problème, il est possible de déclarer une fonction à l’aide d’un prototype. Celui-ci permet de spécifier le type de retour de la fonction, son nombre d’arguments et leur type, mais ne comporte pas le corps de cette fonction. La syntaxe d’un prototype est la suivante.

type nom(paramètres);

Ce qui donne par exemple ceci.

#include <stdio.h>
void bonjour(void);
int main(void)
{
    bonjour();
    return 0;
}
void bonjour(void)
{
    printf("Bonjour !\n");
}

Notez bien le point-virgule à la fin du prototype qui est obligatoire.

Étant donné qu’un prototype ne comprend pas le corps de la fonction qu’il déclare, il n’est pas obligatoire de préciser le nom des paramètres de celles-ci. Ainsi, le prototype suivant est parfaitement correct.

int ppcd(int, int);

Variables globales et classes de stockage

Les variables globales

Il arrive parfois que l’utilisation de paramètres ne soit pas adaptée et que des fonctions soient amenées à travailler sur des données qui doivent leur être communes. Prenons un exemple simple : vous souhaitez compter le nombre d’appels de fonction réalisé durant l’exécution de votre programme. Ceci est impossible à réaliser, sauf à définir une variable dans la fonction main() , la passer en argument de chaque fonction et de faire en sorte que chaque fonction retourne sa valeur augmentée de un, ce qui est très peu pratique.

À la place, il est possible de définir une variable dite « globale » qui sera utilisable par toutes les fonctions. Pour définir une variable globale, il vous suffit de définir une variable en dehors de tout bloc , autrement dit en dehors de toute fonction.

#include <stdio.h>
void fonction(void);
int appels = 0;
void fonction(void)
{
    ++appels;
}
int main(void)
{
    fonction();
    fonction();
    printf("Ce programme a réalisé %d appel(s) de fonction\n", appels);
    return 0;
}
Résultat
Ce programme a réalisé 2 appel(s) de fonction

Comme vous le voyez, nous avons simplement placé la définition de la variable appels en dehors de toute fonction et avant toute définition de fonction de sorte qu’elle soit partagée entre elles.

Le terme « global » est en fait un peu trompeur étant donné que la variable n’est pas globale au programme, mais tout simplement disponible pour toutes les fonctions du fichier dans lequel elle est située. Ce terme est utilisé en opposition aux paramètres et variables des fonctions qui sont dites « locales ».

N’utilisez les variables globales que lorsque cela vous paraît vraiment nécessaire. Ces dernières étant utilisables dans un fichier entier (voire dans plusieurs, nous le verrons un peu plus tard), elles ont tendances à rendre la lecture du code plus difficile.

Les classes de stockage

Les variables locales et les variables globales ont une autre différence de taille : leur classe de stockage. La classe de stockage détermine (entre autre) la durée de vie d’un objet, c’est-à-dire le temps durant lequel celui-ci existera en mémoire.

Classe de stockage automatique

Les variables locales sont par défaut de classe de stockage automatique. Cela signifie qu’elles sont allouées automatiquement à chaque fois que le bloc auquel elles appartiennent est exécuté et qu’elles sont détruites une fois son exécution terminée.

int ppcd(int a, int b)
{
    int min = (a < b) ? a : b;
    for (int i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
            return i;
    return 0;
}

Par exemple, à chaque fois que la fonction ppcd() est appelée, les variables a , b , min sont allouées en mémoire vive et détruites à la fin de l’exécution de la fonction. La variable i quant à elle est allouée au début de la boucle for et détruite à la fin de cette dernière.

Classe de stockage statique

Les variables globales sont toujours de classe de stockage statique. Ceci signifie qu’elles sont allouées au début de l’exécution du programme et sont détruites à la fin de l’exécution de celui-ci. En conséquence, elles conservent leur valeur tout au long de l’exécution du programme.

Également, à l’inverse des autres variables, celles-ci sont initialisées à zéro si elles ne font pas l’objet d’une initialisation. L’exemple ci-dessous est donc correct et utilise deux variables valant zéro.

#include <stdio.h>
int a;
double b;
int main(void)
{
    printf("%d, %f\n", a, b);
    return 0;
}
Résultat
0, 0.000000

Petit bémol tout de même : étant donné que ces variables sont créées au début du programme, elles ne peuvent être initialisées qu’à l’aide de constantes . La présence de variables au sein de l’expression d’initialisation est donc proscrite.

#include <stdio.h>
int a = 20; /* Correct */
double b = a; /* Incorrect */
int main(void)
{
    printf("%d, %f\n", a, b);
    return 0;
}

Modification de la classe de stockage

Il est possible de modifier la classe de stockage d’une variable automatique en précédant sa définition du mot-clé static afin d’en faire une variable statique.

#include <stdio.h>
int compteur(void)
{
    static int n;
    return ++n;
}
int main(void)
{
    compteur();
    printf("n = %d\n", compteur());
    return 0;
}
Résultat
n = 2

Exercices

Afficher un rectangle

Le premier exercice que nous vous proposons consiste à afficher un rectangle dans la console. Voici ce que devra donner l’exécution de votre programme.

Donnez la longueur : 5
Donnez la largeur : 3
***
***
***
***
***

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
void rectangle(int, int);
int main(void)
{
    int longueur;
    int largeur;
    printf("Donnez la longueur : ");
    scanf("%d", &longueur);
    printf("Donnez la largeur : ");
    scanf("%d", &largeur);
    printf("\n");
    rectangle(longueur, largeur);
    return 0;
}
void rectangle(int longueur, int largeur)
{
    for (int i = 0; i < longueur; i++)
    {
        for (int j = 0; j < largeur; j++)
            printf("*");
        printf("\n");
    }
}

Vous pouvez aussi essayer d’afficher le rectangle dans l’autre sens.

Afficher un triangle

Même principe, mais cette fois-ci avec un triangle (rectangle). Le programme devra donner ceci.

Donnez un nombre : 5
*
**
***
****
*****

Bien entendu, la taille du triangle variera en fonction du nombre entré.

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
void triangle(int);
int main(void)
{
    int nombre;
    printf("Donnez un nombre : ");
    scanf("%d", &nombre);
    printf("\n");
    triangle(nombre);
    return 0;
}
void triangle(int nombre)
{
    for (int i = 0; i < nombre; i++)
    {
        for (int j = 0; j <= i; j++)
            printf("*");
        printf("\n");
    }
}

En petites coupures ?

Pour ce dernier exercice, vous allez devoir réaliser un programme qui reçoit en entrée une somme d’argent et donne en sortie la plus petite quantité de coupures nécessaires pour reconstituer cette somme.

Pour cet exercice, vous utiliserez les coupures suivantes :

  • des billets de 100€ ;
  • des billets de 50€ ;
  • des billets de 20€ ;
  • des billets de 10€ ;
  • des billets de 5€ ;
  • des pièces de 2€ ;
  • des pièces de 1€ ;

Ci dessous un exemple de ce que devra donner votre programme une fois terminé.

Entrez une somme : 285
2 billet(s) de 100.
1 billet(s) de 50.
1 billet(s) de 20.
1 billet(s) de 10.
1 billet(s) de 5.

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
int coupure_inferieure(int valeur)
{
    switch (valeur)
    {
    case 100:
        return 50;
    case 50:
        return 20;
    case 20:
        return 10;
    case 10:
        return 5;
    case 5:
        return 2;
    case 2:
        return 1;
    default:
        return 0;
    }
}
void coupure(int somme)
{
    int valeur = 100;
    while (valeur != 0)
    {
        int nb_coupure = somme / valeur;
        if (nb_coupure > 0)
        {
            if (valeur >= 5)
                printf("%d billet(s) de %d.\n", nb_coupure, valeur);
            else
                printf("%d pièce(s) de %d.\n", nb_coupure, valeur);
            somme -= nb_coupure * valeur;
        }
        valeur = coupure_inferieure(valeur);
    }
}
int main(void)
{
    int somme;
    printf("Entrez une somme : ");
    scanf("%d", &somme);
    coupure(somme);
    return 0;
}

Le prochain chapitre sera l’occasion de mettre en pratique ce que nous venons de voir à l’aide d’un second TP.

En résumé
  1. Une fonction permet de rassembler une suite d’instructions qui est amenée à être utilisée plusieurs fois ;
  2. Une fonction peut retourner ou non une valeur et recevoir ou non des paramètres ;
  3. Un paramètre est une variable propre à une fonction, il n’est pas utilisable en dehors de celle-ci ;
  4. Un argument est une expression passée lors d’un l’appel de fonction et dont la valeur est affectée au paramètre correspondant ;
  5. Un prototype permet de déclarer une fonction sans spécifier son corps, il n’est ainsi plus nécessaire de se soucier de l’ordre de définition des fonctions ;
  6. Il est possible de définir une variable « globale » en plaçant sa définition en dehors de tout bloc (et donc de toute fonction) ;
  7. Une variable définie au sein d’un bloc est de classe de stockage automatique, alors qu’une variable définie en dehors de tout bloc est de classe de stockage statique ;
  8. Une variable de classe de stockage automatique est détruite une fois que l’exécution du bloc auquel elle appartient est terminée ;
  9. Une variable de classe de stockage statique existe durant toute l’exécution du programme ;
  10. Il est possible de modifier la classe de stockage d’une variable de classe de stockage automatique à l’aide du mot-clé static ;


TP : une calculatrice basique - Le langage C • Tutoriels • Zeste de Savoir

TP : une calculatrice basique

Objectif

Votre objectif sera de réaliser une calculatrice basique pouvant calculer une somme, une soustraction, une multiplication, une division, le reste d’une division entière, une puissance, une factorielle, le PGCD et le PPCD.

Celle-ci attendra une entrée formatée suivant la notation polonaise inverse. Autrement dit, les opérandes d’une opération seront entrés avant l’opérateur, par exemple comme ceci pour la somme de quatre et cinq : 4 5 + .

Elle devra également retenir le résultat de l’opération précédente et déduire l’utilisation de celui-ci en cas d’omission d’un opérande. Plus précisément, si l’utilisateur entre par exemple 5 + , vous devrez déduire que le premier opérande de la somme est le résultat de l’opération précédente (ou zéro s’il n’y en a pas encore eu).

Chaque opération sera identifiée par un symbole ou une lettre, comme suit :

  • addition : + ;
  • soustraction : - ;
  • multiplication : * ;
  • division : / ;
  • reste de la division entière : % ;
  • puissance : ^ ;
  • factorielle : ! ;
  • PGCD : g ;
  • PPCD : p .

Le programme doit s’arrêter lorsque la lettre « q » est spécifiée comme opération (avec ou sans opérande).

Préparation

Précisions concernant scanf

Pourquoi utiliser la notation polonaise inverse et non l’écriture habituelle ?

Parce qu’elle va vous permettre de bénéficier d’une caractéristique intéressante de la fonction scanf() : sa valeur de retour. Nous anticipons un peu sur les chapitres suivants, mais sachez que la fonction scanf() retourne une valeur entière qui correspond au nombre de conversions réussies. Une conversion est réussie si ce qu’entre l’utilisateur correspond à l’indicateur de conversion.

Ainsi, si nous souhaitons récupérer un entier à l’aide de l’indicateur d , la conversion sera réussie si l’utilisateur entre un nombre (par exemple 2) alors qu’elle échouera s’il entre une lettre ou un signe de ponctuation.

Grâce à cela, vous pourrez détecter facilement s’il manque ou non un opérande pour une opération.

Lorsqu’une conversion échoue, la fonction scanf() arrête son exécution. Aussi, s’il y avait d’autres conversions à effectuer après celle qui a avorté, elles ne seront pas réalisées.

double a;
double b;
char op;
scanf("%lf %lf %c", &a, &b, &op);

Dans le code ci-dessus, si l’utilisateur entre 7 * , la fonction scanf() retournera 1 et n’aura lu que le nombre 7. Il sera nécessaire de l’appeler une seconde fois pour que le symbole * soit récupéré.

Petit bémol tout de même : les symboles + et - sont considérés comme des débuts de nombre valables (puisque vous pouvez par exemple entrer -2). Dès lors, si vous souhaitez additionner ou soustraire un nombre au résultat de l’opération précédente, vous devrez doubler ce symbole. Pour ajouter cinq cela donnera donc : 5 ++ .

Les puissances

Pour élever un nombre à une puissance donnée (autrement dit, pour calculer x y x y x^y x y ), nous allons avoir besoin d’une nouvelle partie de la bibliothèque standard dédiée aux fonctions mathématiques de base. Le fichier d’en-tête de la bibliothèque mathématique se nomme <math.h> et contient, entre autres, la déclaration de la fonction pow() .

double pow(double x, double y);

Cette dernière prend deux arguments : la base et l’exposant.

L’utilisation de la bibliothèque mathématique requiert d’ajouter l’option -lm lors de la compilation, par exemple comme ceci : zcc -lm main.c

La factorielle

La factorielle d’un nombre est égale au produit des nombres entiers positifs et non nuls inférieurs ou égaux à ce nombre. La factorielle de quatre équivaut donc à 1 * 2 * 3 * 4 , donc vingt-quatre. Cette fonction n’est pas fournie par la bibliothèque standard, il vous faudra donc la programmer (pareil pour le PGCD et le PPCD que nous avons vus dans les chapitres précédents).

Par convention, la factorielle de zéro est égale à un.

Exemple d’utilisation

> 5 6 +
11.000000
> 4 *
44.000000
> 2 /
22.000000
> 5 2 %
1.000000
> 2 5 ^
32.000000
> 1 ++
33.000000
> 5 !
120.000000

Derniers conseils

Nous vous conseillons de récupérer les nombres sous forme de double . Cependant, gardez bien à l’esprit que certaines opérations ne peuvent s’appliquer qu’à des entiers : le reste de la division entière, la factorielle, le PGCD et le PPCD. Il vous sera donc nécessaire d’effectuer des conversions.

Également, notez bien que la factorielle ne s’applique qu’à un seul opérande à l’inverse de toutes les autres opérations.

Bien, vous avez à présent toutes les cartes en main : au travail ! :)

Correction

Alors ? Pas trop secoué ? :D
Bien, voyons à présent la correction.

#include <math.h>
#include <stdio.h>
#include <stdlib.h>
long long pgcd(long long, long long);
long long ppcd(long long, long long);
unsigned long long factorielle(unsigned long long);
long long pgcd(long long a, long long b)
{
    long long r = a % b;
    while (r != 0)
    {
            a = b;
            b = r;
            r = a % b;
    }
    return b;
}
long long ppcd(long long a, long long b)
{
    long long min = (a < b) ? a : b;
    for (long long i = 2; i <= min; ++i)
        if (a % i == 0 && b % i == 0)
            return i;
    return 0;
}
unsigned long long factorielle(unsigned long long a)
{
    unsigned long long r = 1;
    for (unsigned long long i = 2; i <= a; ++i)
        r *= i;
    return r;
}
int
main(void)
{
    double res = 0.;
    while (1)
    {
        double a;
        double b;
        char op;
        printf("> ");
        int n = scanf("%lf %lf %c", &a, &b, &op);
        if (n <= 1)
        {
            scanf("%c", &op);
            b = a;
            a = res;
        }
        if (op == 'q')
            break;
        switch (op)
        {
        case '+':
            res = a + b;
            break;
        case '-':
            res = a - b;
            break;
        case '*':
            res = a * b;
            break;
        case '/':
            res = a / b;
            break;
        case '%':
            res = (long long)a % (long long)b;
            break;
        case '^':
            res = pow(a, b);
            break;
        case '!':
            res = factorielle((n == 0) ? a : b);
            break;
        case 'g':
            res = pgcd(a, b);
            break;
        case 'p':
            res = ppcd(a, b);
            break;
        }
        printf("%lf\n", res);
    }
    return 0;
}

Commençons par la fonction main() . Nous définissons plusieurs variables :

  • res , qui correspond au résultat de la dernière opération réalisée (ou zéro s’il n’y en a pas encore eu) ;
  • a et b , qui représentent les éventuels opérandes fournis ;
  • op , qui retient l’opération demandée ; et
  • n , qui est utilisée pour retenir le retour de la fonction scanf() .

Ensuite, nous entrons dans une boucle infinie (la condition étant toujours vraie puisque valant un) où nous demandons à l’utilisateur d’entrer l’opération à réaliser et les éventuels opérandes. Nous vérifions ensuite si un seul opérande est fourni ou aucun (ce qui se déduit, respectivement, d’un retour de la fonction scanf() valant un ou zéro). Si c’est le cas, nous appelons une seconde fois scanf() pour récupérer l’opérateur. Puis, la valeur de a est attribuée à b et la valeur de res à a .

Si l’opérateur utilisé est q , alors nous quittons la boucle et par la même occasion le programme. Notez que nous n’avons pas pu effectuer cette vérification dans le corps de l’instruction switch qui suit puisque l’instruction break nous aurait fait quitter celui-ci et non la boucle.

Enfin, nous réalisons l’opération demandée au sein de l’instruction switch , nous stockons le résultat dans la variable res et l’affichons. Remarquez que l’utilisation de conversions explicites n’a été nécessaire que pour le calcul du reste de la division entière. En effet, dans les autres cas (par exemple lors de l’affectation à la variable res ), il y a des conversions implicites.

Nous avons utilisé le type long long lors des calculs nécessitants des nombres entiers afin de disposer de la plus grande capacité possible. Par ailleurs, nous avons employé le type unsigned long long pour la fonction factorielle puisque celle-ci n’opère que sur des nombres strictement positifs.

Ce chapitre nous aura permis de revoir la plupart des notions des chapitres précédents. Dans le chapitre suivant, nous verrons comment découper nos projets en plusieurs fichiers.


 Découper son projet - Le langage C • Tutoriels • Zeste de Savoir

Découper son projet

Portée et masquage

La notion de portée

Avant de voir comment diviser nos programmes en plusieurs fichiers, il est nécessaire de vous présenter une notion importante, celle de portée. La portée d’une variable est la partie du programme où cette dernière est utilisable. Il existe plusieurs types de portées, cependant nous n’en verrons que deux :

  • au niveau d’un bloc ;
  • au niveau d’un fichier.

Au niveau d’un bloc

Une portée au niveau d’un bloc signifie qu’une variable est utilisable, visible de sa déclaration jusqu’à la fin du bloc dans lequel elle est déclarée. Illustration.

#include <stdio.h>

int main(void)
{
    {
        int nombre = 3;
        printf("%d\n", nombre);
    }

    /* Incorrect ! */
    printf("%d\n", nombre);
    return 0;
}

Dans ce code, la variable nombre est déclarée dans un sous-bloc. Sa portée est donc limitée à ce dernier et elle ne peut pas être utilisée en dehors.

Au niveau d’un fichier

Une portée au niveau d’un fichier signifie qu’une variable est utilisable, visible, de sa déclaration jusqu’à la fin du fichier dans lequel elle est déclarée. En fait, il s’agit de la portée des variables « globales » dont nous avons parlé dans le chapitre sur les fonctions.

#include <stdio.h>

int nombre = 3;

int triple(void)
{
    return nombre * 3;
}

int main(void)
{
    nombre = triple();
    printf("%d\n", nombre);
    return 0;
}

Dans ce code, la variable nombre a une portée au niveau du fichier et peut par conséquent être aussi bien utilisée dans la fonction triple() que dans la fonction main().

La notion de masquage

En voyant les deux types de portées, vous vous êtes peut-être posé la question suivante : que se passe-t-il s’il existe plusieurs variables de même nom ? Hé bien, cela dépend de la portée de ces dernières. Si elles ont la même portée comme dans l’exemple ci-dessous, alors le compilateur sera incapable de déterminer à quelle variable ou à quelle fonction le nom fait référence et, dès lors, retournera une erreur.

int main(void)
{
    int nombre = 10;
    int nombre = 20;

    return 0;
}

En revanche, si elles ont des portées différentes, alors celle ayant la portée la plus petite sera privilégiée, on dit qu’elle masque celle(s) de portée(s) plus grande(s). Autrement dit, dans l’exemple qui suit, c’est la variable du bloc de la fonction main() qui sera affichée.

#include <stdio.h>

int nombre = 10;

int main(void)
{
    int nombre = 20;

    printf("%d\n", nombre);
    return 0;
}

Notez que nous disons : « celle(s) de portée plus petite » car les variables déclarées dans un sous-bloc ont une portée plus faible que celle déclarée dans un bloc supérieur. Ainsi, le code ci-dessous est parfaitement valide et affichera 30.

#include <stdio.h>

int nombre = 10;

int main(void)
{
    int nombre = 20;

    if (nombre == 20)
    {
        int nombre = 30;

        printf("%d\n", nombre);
    }

    return 0;
}

Diviser pour mieux régner

Les fonctions

Dans l’extrait précédent, nous avions, entre autres, créé une fonction triple() que nous avons placée dans le même fichier que la fonction main(). Essayons à présent de les répartir dans deux fichiers distincts. Pour ce faire, il vous suffit de créer un second fichier avec l’extension « .c ». Dans notre cas, il s’agira de « main.c » et de « autre.c ».

autre.c
int triple(int nombre)
{
    return nombre * 3;
}
main.c
int main(void)
{
    int nombre = triple(3);
    return 0;
}

La compilation se réalise de la même manière qu’auparavant, si ce n’est qu’il vous est nécessaire de spécifier les deux noms de fichier : zcc main.c autre.c. À noter que vous pouvez également utiliser une forme raccourcie : zcc *.c, où *.c correspond à tous les fichiers portant l’extension « .c » du dossier courant.

Si vous testez ce code, vous aurez droit à un bel avertissement de votre compilateur du type « implicit declaration of function 'triple' ». Quel est le problème ? Le problème est que la fonction triple() n’est pas déclarée dans le fichier main.c et que le compilateur ne la connaît donc pas lorsqu’il compile le fichier. Pour corriger cette situation, nous devons déclarer la fonction en signalant au compilateur que cette dernière se situe dans un autre fichier. Pour ce faire, nous allons inclure le prototype de la fonction triple() dans le fichier main.c en le précédant du mot-clé extern, qui signifie que la fonction est externe au fichier.

autre.c
int triple(int nombre)
{
    return nombre * 3;
}
main.c
extern int triple(int nombre);

int main(void)
{
    int nombre = triple(3);

    return 0;
}

En terme technique, on dit que la fonction triple() est définie dans le fichier « autre.c » (car c’est là que se situe le corps de la fonction) et qu’elle est déclarée dans le fichier « main.c ». Sachez qu’une fonction ne peut être définie qu’une seule et unique fois.

Pour information, notez que le mot-clé extern est facultatif devant un prototype (il est implicitement inséré par le compilateur). Nous vous conseillons cependant de l’utiliser par soucis de clarté et de symétrie avec les déclarations de variables (voyez ci-dessous).

Les variables

La même méthode peut être appliquée aux variables, mais uniquement à celle ayant une portée au niveau d’un fichier. Également, à l’inverse des fonctions, il est plus difficile de distinguer une définition d’une déclaration de variable (elles n’ont pas de corps comme les fonctions). La règle pour les différencier est qu’une déclaration sera précédée du mot-clé extern alors que la définition non. C’est à vous de voir dans quel fichier vous souhaitez définir la variable, mais elle ne peut être définie qu’une seule et unique fois. Enfin, sachez que seule la définition peut comporter une initialisation. Ainsi, cet exemple est tout à fait valide.

autre.c
int nombre = 10;   /* Une définition */
extern int autre;  /* Une déclaration */
main.c
extern int nombre;  /* Une déclaration */
int autre = 10;     /* Une définition */

Alors que celui-ci, non.

autre.c
int nombre = 10;        /* Il existe une autre définition */
extern int autre = 10;  /* Une déclaration ne peut pas comprendre une initialisation */
main.c
int nombre = 20;  /* Il existe une autre définition */
int autre = 10;   /* Une définition */

On m’aurait donc menti ?

Nous vous avons dit plus haut qu’il n’était possible de définir une variable ou une fonction qu’une seule fois, mais en fait, ce n’est pas tout à fait vrai … Il est possible de rendre une variable (ayant une portée au niveau d’un fichier) ou une fonction locale à un fichier en précédant sa définition du mot-clé static. De cette manière, la variable ou la fonction est interne au fichier où elle est définie et n’entre pas en conflit avec les autres variables ou fonctions locales à d’autres fichiers. La contrepartie est que la variable ou la fonction ne peut être utilisée que dans le fichier où elle est définie (c’est assez logique). Ainsi, l’exemple suivant est tout à fait correct et affichera 20.

autre.c
static int nombre = 10;
main.c
#include <stdio.h>

static int nombre = 20;

int main(void)
{
    printf("%d\n", nombre);
    return 0;
}

Ne confondez pas l’utilisation du mot-clé static visant à modifier la classe de stockage d’une variable automatique avec celle permettant de limiter l’utilisation d’une variable globale à un seul fichier !

Les fichiers d'en-têtes

Pour terminer ce chapitre, il ne nous reste plus qu’à voir les fichiers d’en-têtes.

Jusqu’à présent, lorsque vous voulez utiliser une fonction ou une variable définie dans un autre fichier, vous insérez sa déclaration dans le fichier ciblé. Seulement voilà, si vous utilisez dix fichiers et que vous décidez un jour d’ajouter ou de supprimer une fonction ou une variable ou encore de modifier une déclaration, vous vous retrouvez Gros-Jean comme devant et vous êtes bon pour modifier les dix fichiers, ce qui n’est pas très pratique…

Pour résoudre ce problème, on utilise des fichiers d’en-têtes (d’extension « .h »). Ces derniers contiennent conventionnellement des déclarations de fonctions et de variables et sont inclus via la directive #include dans les fichiers qui utilisent les fonctions et variables en question.

Les fichiers d’en-têtes n’ont pas besoin d’être spécifiés lors de la compilation, ils seront automatiquement inclus.

La structure d’un fichier d’en-tête est généralement de la forme suivante.

#ifndef CONSTANTE_H
#define CONSTANTE_H

/* Les déclarations */

#endif

Les directives du préprocesseur sont là pour éviter les inclusions multiples : vous devez les utiliser pour chacun de vos fichiers d’en-têtes. Vous pouvez remplacer CONSTANTE par ce que vous voulez, le plus simple et le plus fréquent étant le nom de votre fichier, par exemple AUTRE_H si votre fichier se nomme « autre.h ». Voici un exemple d’utilisation de fichiers d’en-têtes.

autre.h
#ifndef AUTRE_H
#define AUTRE_H

extern int triple(int nombre);

#endif
autre.c
#include "autre.h"

int triple(int nombre)
{
    return nombre * 3;
}
main.c
#include "autre.h"

int main(void)
{
    int nombre = triple(3);
    return 0;
}

Plusieurs remarques à propos de ce code :

  • dans la directive d’inclusion, les fichiers d’en-têtes sont entre guillemets et non entre chevrons comme les fichiers d’en-têtes de la bibliothèque standard ;
  • les fichiers sources et d’en-têtes correspondants portent le même nom ;
  • nous vous conseillons d’inclure le fichier d’en-tête dans le fichier source correspondant (dans mon cas « autre.h » dans « autre.c ») afin d’éviter des problèmes de portée.


La gestion d'erreurs (1) - Le langage C • Tutoriels • Zeste de Savoir

La gestion d'erreurs (1)

Détection d'erreurs

La première chose à faire pour gérer d’éventuelles erreurs lors de l’exécution, c’est avant tout de les détecter. Par exemple, quand vous exécutez une fonction et qu’une erreur a lieu lors de son exécution, celle-ci doit vous prévenir d’une manière ou d’une autre. Et elle peut le faire de deux manières différentes.

Valeurs de retour

Nous l’avons vu dans les chapitres précédents : certaines fonctions, comme scanf() , retournent un nombre (souvent un entier) alors qu’elles ne calculent pas un résultat comme la fonction pow() par exemple. Vous savez dans le cas de scanf() que cette valeur représente le nombre de conversions réussies, cependant cela va plus loin que cela : cette valeur vous signifie si l’exécution de la fonction s’est bien déroulée.

Scanf

En fait, la fonction scanf() retourne le nombre de conversions réussies ou un nombre inférieur si elles n’ont pas toutes été réalisées ou, enfin, un nombre négatif en cas d’erreur .

Ainsi, si nous souhaitons récupérer deux entiers et être certains que scanf() les a récupérés, nous pouvons utiliser le code suivant.

#include <stdio.h>
int main(void)
{
    int x;
    int y;
    printf("Entrez deux nombres : ");
    if (scanf("%d %d", &x, &y) == 2)
        printf("Vous avez entre : %d et %d\n", x, y);
    return 0;
}
Résultat
Entrez deux nombres : 1 2
Vous avez entre : 1 et 2
Entrez deux nombres : 1 a

Comme vous pouvez le constater, le programme n’exécute pas l’affichage des nombres dans le dernier cas, car scanf() n’a pas réussi à réaliser deux conversions.

Main

Maintenant que vous savez cela, regarder bien votre fonction main() .

int main(void)
{
    return 0;
}

Vous ne voyez rien qui vous interpelle ? :)

Oui, vous avez bien vu, elle retourne un entier qui, comme pour scanf() , sert à indiquer la présence d’erreur. En fait, il y a deux valeurs possibles :

  • EXIT_SUCCESS (ou zéro, cela revient au même), qui indique que tout s’est bien passé ; et
  • EXIT_FAILURE , qui indique un échec du programme.

Ces deux constantes sont définies dans l’en-tête <stdlib.h> .

Les autres fonctions

Sachez que scanf() , printf() et main() ne sont pas les seules fonctions qui retournent des entiers, en fait quasiment toutes les fonctions de la bibliothèque standard le font.

Ok, mais je fais comment pour savoir ce que retourne une fonction ?

À l’aide de la documentation. Vous disposez de la norme (enfin, du brouillon de celle-ci) qui reste la référence ultime, sinon vous pouvez également utiliser un moteur de recherche avec la requête man nom_de_fonction afin d’obtenir les informations dont vous avez besoin.

Si vous êtes anglophobe, une traduction française de diverses descriptions est disponible à cette adresse, vous les trouverez à la section trois.

Variable globale errno

Le retour des fonctions est un vecteur très pratique pour signaler une erreur. Cependant, il n’est pas toujours utilisable. En effet, nous avons vu lors du second TP la fonction mathématique pow() . Or, cette dernière utilise déjà son retour pour transmettre le résultat d’une opération. Comment faire dès lors pour signaler un problème ?

Une première idée serait d’utiliser une valeur particulière, comme zéro par exemple. Toutefois, ce n’est pas satisfaisant puisque, dans le cas de la fonction pow() , elle peut parfaitement retourner zéro lors d’un fonctionnement normal. Que faire alors ?

Dans une telle situation, il ne reste qu’une seule solution : utiliser un autre canal, en l’occurrence une variable globale. La bibliothèque standard fourni une variable globale nommée errno (elle est déclarée dans l’en-tête <errno.h> ) qui permet à différentes fonctions d’indiquer une erreur en modifiant la valeur de celle-ci.

Une valeur de zéro indique qu’aucune erreur n’est survenue.

Les fonctions mathématiques recourent abondamment à cette fonction. Prenons l’exemple suivant.

#include <errno.h>
#include <stdio.h>
int main(void)
{
    errno = 0;
    double x = pow(-1, 0.5);
    if (errno == 0)
        printf("x = %f\n", x);
    return 0;
}

L’appel revient à demander le résultat de l’expression − 1 1 2 − 1 1 2 -1^\frac{1}{2} − 1 2 1 ​ , autrement dit, de cette expression : − 1 − − − √ − 1 \sqrt{-1} − 1 ​ , ce qui est impossible dans l’ensemble des réels. Aussi, la fonction pow() modifie la variable errno pour vous signifier qu’elle n’a pas pu calculer l’expression demandée.

Une petite précision concernant ce code et la variable errno : celle-ci doit toujours être mise à zéro avant d’appeler une fonction qui est susceptible de la modifier, ceci afin de vous assurer qu’elle ne contient pas la valeur qu’une autre fonction lui a assignée auparavant. Imaginez que vous ayez précédemment appelé la fonction pow() et que cette dernière a échoué, si vous l’appelez à nouveau, la valeur de errno sera toujours celle assignée lors de l’appel précédent.

Notez que la bibliothèque standard ne prévoit en fait que trois valeurs d’erreur possibles pour errno : EDOM (pour le cas où le résultat d’une fonction mathématique est impossible), ERANGE (en cas de dépassement de capacité, nous y reviendrons plus tard) et EILSEQ (pour les erreurs de conversions, nous en reparlerons plus tard également). Ces deux constantes sont définies dans l’en-tête <errno.h> .

Prévenir l'utilisateur

Savoir qu’une erreur s’est produite, c’est bien, le signaler à l’utilisateur, c’est mieux. Ne laissez pas votre utilisateur dans le vide, s’il se passe quelque chose, dites le lui.

#include <stdio.h>
int main(void)
{
    int x;
    int y;
    printf("Entrez deux nombres : ");
    if (scanf("%d %d", &x, &y) == 2)
        printf("Vous avez entré : %d et %d\n", x, y);
    else
        printf("Vous devez saisir deux nombres !\n");
    return 0;
}
Résultat
Entrez deux nombres : a b
Vous devez saisir deux nombres !
Entrez deux nombres : 1 2
Vous avez entre : 1 et 2

Simple, mais tellement plus agréable. ;)

Un exemple d'utilisation des valeurs de retour

Maintenant que vous savez tout cela, il vous est possible de modifier le code utilisant la fonction scanf() pour vérifier si celle-ci a réussi et, si ce n’est pas le cas, préciser à l’utilisateur qu’une erreur est survenue et quitter la fonction main() en retournant la valeur EXIT_FAILURE .

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int x;
    int y;
    printf("Entrez deux nombres : ");
    if (scanf("%d %d", &x, &y) != 2)
    {
        printf("Vous devez saisir deux nombres !\n");
        return EXIT_FAILURE;
    }
    printf("Vous avez entré : %d et %d\n", x, y);
    return 0;
}

Ceci nous permet de réduire un peu la taille de notre code en éliminant directement les cas d’erreurs.


Les pointeurs - Le langage C • Tutoriels • Zeste de Savoir

Les pointeurs

Présentation

Avant de vous présenter le concept de pointeur, un petit rappel concernant la mémoire s’impose (n’hésitez pas à relire le chapitre sur les variables si celui-ci s’avère insuffisant).

Souvenez-vous : toute donnée manipulée par l’ordinateur est stockée dans sa mémoire, plus précisément dans une de ses différentes mémoires (registre(s), mémoire vive, disque(s) dur(s), etc.). Cependant, pour utiliser une donnée, nous avons besoin de savoir où elle se situe, nous avons besoin d’une référence vers cette donnée. Dans la plupart des cas, cette référence est en fait une adresse mémoire qui indique la position de la donnée dans la mémoire vive.

Les pointeurs

Si l’utilisation des références peut être implicite (c’est par exemple le cas lorsque vous manipulez des variables), il est des cas où elle doit être explicite. C’est à cela que servent les pointeurs : ce sont des variables dont le contenu est une adresse mémoire (une référence, donc).

+---------+---------+
| Adresse | Contenu |
+---------+---------+
|    1    |   56.7  |
+---------+---------+
|    2    |   78    |
+---------+---------+
|    3    |   0     |
+---------+---------+
|    4    |   101   |
+---------+----+----+
               |
     +---------+
     |
+----v----+---------+
|   101   |   42    |
+---------+---------+
|   102   |   3.14  |
+---------+---------+
|   103   |   999   |
+---------+---------+

Comme vous pouvez le voir sur le schéma ci-dessus, le contenu à l’adresse 4 est lui-même une adresse et référence le contenu situé à l’adresse 101.

Utilité des pointeurs

Techniquement, il y a trois utilisations majeures des pointeurs en C :

  • le passage de références à des fonctions ;
  • la manipulation de données complexes ;
  • l’allocation dynamique de mémoire.

Passage de références à des fonctions

Rappelez-vous du chapitre sur les fonctions : lorsque vous fournissez un argument lors d’un appel, la valeur de celui-ci est affectée au paramètre correspondant, paramètre qui est une variable propre à la fonction appelée. Toutefois, il est parfois souhaitable de modifier une variable de la fonction appelante . Dès lors, plutôt que de passer la valeur de la variable en argument, c’est une référence vers celle-ci qui sera envoyée à la fonction.

Manipulation de données complexes

Jusqu’à présent, nous avons manipulé des données simples : int , double , char , etc. Cependant, le C nous permet également d’utiliser des données plus complexes qui sont en fait des agrégats (un regroupement si vous préférez) de données simples. Or, il n’est possible de manipuler ces agrégats qu’en les parcourant donnée simple par donnée simple, ce qui requiert de disposer d’une référence vers les données qui le composent.

Nous verrons les agrégats plus en détails lorsque nous aborderons les structures et les tableaux.

L’allocation dynamique de mémoire

Il n’est pas toujours possible de savoir quelle quantité de mémoire sera utilisée par un programme. En effet, si vous prenez le cas d’un logiciel de dessin, ce dernier ne peut pas prévoir quelle sera la taille des images qu’il va devoir manipuler. Pour pallier ce problème, les programmes recourent au mécanisme de l’allocation dynamique de mémoire : ils demandent de la mémoire au système d’exploitation lors de leur exécution. Pour que cela fonctionne, le seul moyen est que le système d’exploitation fournisse au programme une référence vers la zone allouée.

Déclaration et initialisation

La syntaxe pour déclarer un pointeur est la suivante.

type *nom_du_pointeur;

Par exemple, si nous souhaitons créer un pointeur sur int (c’est-à-dire un pointeur pouvant stocker l’adresse d’un objet de type int ) et que nous voulons le nommer « ptr », nous devons écrire ceci.

int *ptr;

L’astérisque peut être entourée d’espaces et placée n’importe où entre le type et l’identificateur. Ainsi, les trois définitions suivantes sont identiques.

int *ptr;
int * ptr;
int* ptr;

Notez bien qu’un pointeur est toujours typé. Autrement dit, vous aurez toujours un pointeur sur (ou vers) un objet d’un certain type ( int , double , char , etc.).

Initialisation

Un pointeur, comme une variable, ne possède pas de valeur par défaut, il est donc important de l’initialiser pour éviter d’éventuels problèmes. Pour ce faire, il est nécessaire de recourir à l’opérateur d’adressage (ou de référencement) : & qui permet d’obtenir l’adresse d’un objet. Ce dernier se place derrière l’objet dont l’adresse souhaite être obtenue. Par exemple comme ceci.

int a = 10;
int *p;
p = &a;

Ou, plus directement, comme cela.

int a = 10;
int *p = &a;

Faites bien attention à ne pas mélanger différents types de pointeurs ! Un pointeur sur int n’est pas le même qu’un pointeur sur long ou qu’un pointeur sur double . De même, n’affectez l’adresse d’un objet qu’à un pointeur du même type.

int a;
double b;
int *p = &b; /* Faux */
int *q = &a; /* Correct */
double *r = p; /* Faux */

Pointeur nul

Vous souvenez-vous du chapitre sur la gestion d’erreurs ? Dans ce dernier, nous vous avons dit que, le plus souvent, les fonctions retournaient une valeur particulière en cas d’erreur. Quid de celles qui retournent un pointeur ? Existe-t-il une valeur spéciale qui puisse représenter une erreur ou bien sommes-nous condamnés à utiliser une variable globale comme errno ?

Heureusement pour nous, il existe un cas particulier : les pointeurs nuls. Un pointeur nul est tout simplement un pointeur contenant une adresse invalide. Cette adresse invalide dépend de votre système d’exploitation, mais elle est la même pour tous les pointeurs nuls. Ainsi, deux pointeurs nuls ont une valeur égale.

Pour obtenir cette adresse invalide, il vous suffit de convertir explicitement zéro vers le type de pointeur voulu. Ainsi, le pointeur suivant est un pointeur nul.

int *p = (int *)0;

Rappelez-vous qu’il y a conversion implicite vers le type de destination dans le cas d’une affectation. La conversion est donc superflue dans ce cas-ci.

La constante NULL

Afin de clarifier un peu les codes sources, il existe une constante définie dans l’en-tête <stddef.h> : NULL . Celle-ci peut être utilisée partout où un pointeur nul est attendu.

int *p = NULL; /* Un pointeur nul */

Utilisation

Indirection (ou déréférencement)

Maintenant que nous savons récupérer l’adresse d’un objet et l’affecter à un pointeur, voyons le plus intéressant : accéder à cet objet ou le modifier via le pointeur. Pour y parvenir, nous avons besoin de l’opérateur d’indirection (ou de déréférencement) : * .

Le symbole * n’est pas celui de la multiplication ? :euh:

Si, c’est aussi le symbole de la multiplication. Toutefois, à l’inverse de l’opérateur de multiplication, l’opérateur d’indirection ne prend qu’un seul opérande (il n’y a donc pas de risque de confusion).

L’opérateur d’indirection attend un pointeur comme opérande et se place juste derrière celui-ci. Une fois appliqué, ce dernier nous donne accès à la valeur de l’objet référencé par le pointeur, aussi bien pour la lire que pour la modifier.

Dans l’exemple ci-dessous, nous accédons à la valeur de la variable a via le pointeur p .

int a = 10;
int *p = &a;
printf("a = %d\n", *p);
Résultat
a = 10

À présent, modifions la variable a à l’aide du pointeur p .

int a = 10;
int *p = &a;
*p = 20;
printf("a = %d\n", a);
Résultat
a = 20

Comme pour n’importe quelle variable, il est possible de déclarer un pointeur comme constant. Cependant, puisqu’un pointeur référence un objet, il peut également être déclaré comme un pointeur vers un objet constant. Pour ce faire, la position du mot-clé const est importante.

Si le mot-clé est devant l’identificateur et derrière le symbole * , alors il s’agit d’un pointeur constant.

int * const ptr; /* Un pointeur constant sur int. */

Si le mot-clé est devant le symbole * et derrière le type référencé, alors il s’agit d’un pointeur vers un objet constant.

int const *ptr; /* Pointeur sur int constant. */

Enfin, ces deux notations peuvent être combinées pour créer un pointeur constant vers un objet constant.

int const * const ptr; /* Pointeur constant sur int constant. */

Passage comme argument

Voici un exemple de passage de pointeurs en arguments d’une fonction.

#include <stdio.h>
void test(int *pa, int *pb)
{
    *pa = 10;
    *pb = 20;
}
int main(void)
{
    int a;
    int b;
    int *pa = &a;
    int *pb = &b;
    test(&a, &b);
    test(pa, pb);
    printf("a = %d, b = %d\n", a, b);
    printf("a = %d, b = %d\n", *pa, *pb);
    return 0;
}
Résultat
a = 10, b = 20
a = 10, b = 20

Remarquez que les appels test(&a, &b) et test(pa, pb) réalisent la même opération.

Retour de fonction

Pour terminer, sachez qu’une fonction peut également retourner un pointeur. Cependant, faites attention : l’objet référencé par le pointeur doit toujours exister au moment de son utilisation ! L’exemple ci-dessous est donc incorrect étant donnée que la variable n est de classe de stockage automatique et qu’elle n’existe donc plus après l’appel à la fonction ptr() .

#include <stdio.h>
int *ptr(void)
{
    int n;
    return &n;
}
int main(void)
{
    int *p = ptr();
    *p = 10;
    printf("%d\n", *p);
    return 0;
}

L’exemple devient correct si n est de classe de stockage statique.

Pointeur de pointeur

Au même titre que n’importe quel autre objet, un pointeur a lui aussi une adresse. Dès lors, il est possible de créer un objet pointant sur ce pointeur : un pointeur de pointeur.

int a = 10;
int *pa = &a;
int **pp = &pa;

Celui-ci s’utilise de la même manière qu’un pointeur si ce n’est qu’il est possible d’opérer deux indirections : une pour atteindre le pointeur référencé et une seconde pour atteindre la variable sur laquelle pointe le premier pointeur.

#include <stdio.h>
int main(void)
{
    int a = 10;
    int *pa = &a;
    int **pp = &pa;
    printf("a = %d\n", **pp);
    return 0;
}

Ceci peut continuer à l’infini pour concevoir des pointeurs de pointeur de pointeur de pointeur de… Bref, vous avez compris le principe. :p

Pointeurs génériques et affichage

Le type void

Vous avez déjà recontré le mot-clé void lorsque nous avons parlé des fonctions, ce dernier permet d’indiquer qu’une fonction n’utilise aucun paramètre et/ou ne retourne aucune valeur. Toutefois, nous n’avons pas tout dit à son sujet : void est en fait un type, au même titre que int ou double .

Et il représente quoi ce type, alors ?

Hum … rien (d’où son nom). :-°
En fait, il s’agit d’un type dit « incomplet », c’est à dire que la taille de ce dernier n’est pas calculable et qu’il n’est pas utilisable dans des expressions. Quel est l’intérêt de la chose me direz-vous ? Permettre de créer des pointeurs « génériques » (ou « universels »).

En effet, nous venons de vous dire qu’un pointeur devait toujours être typé. Cependant, cela peut devenir gênant si vous souhaitez créer une fonction qui doit pouvoir travailler avec n’importe quel type de pointeur (nous verrons un exemple très bientôt). C’est ici que le type void intervient : un pointeur sur void est considéré comme un pointeur générique, ce qui signifie qu’il peut référencer n’importe quel type d’objet.

En conséquence, il est possible d’affecter n’importe quelle adresse d’objet à un pointeur sur void et d’affecter un pointeur sur void à n’importe quel autre pointeur (et inversement).

int a;
double b;
void *p;
double *r;
p = &a; /* Correct */
p = &b; /* Correct */
r = p; /* Correct */

Afficher une adresse

Il est possible d’afficher une adresse à l’aide de l’indicateur de conversion p de la fonction printf() . Ce dernier attend en argument un pointeur sur void . Vous voyez ici l’intérêt d’un pointeur générique : un seul indicateur suffit pour afficher tous les types de pointeurs.

Notez que l’affichage s’effectue le plus souvent en hexadécimal.

int a;
int *p = &a;
printf("%p == %p\n", (void *)&a, (void *)p);

Tant que nous y sommes, profitons en pour voir quelle est l’adresse invalide de notre système.

printf("%p\n", (void *)0); /* Comme ceci */
printf("%p\n", (void *)NULL) /* Ou comme cela */
Afficher/Masquer le contenu masqué

Oui, le plus souvent, il s’agit de zéro. :p

Exercice

Pour le moment, tout ceci doit sans doute vous paraître quelques peu abstrait et sans doute inutile. Toutefois, rassurez-vous, cela vous semblera plus clair après les chapitres suivants.

En attendant, nous vous proposons un petit exercice mettant en pratique les pointeurs : programmez une fonction nommée « swap », dont le rôle est d’échanger la valeur de deux variables de type int . Autrement dit, la valeur de la variable « a » doit devenir celle de « b » et la valeur de « b », celle de « a ».

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
void swap(int *, int *);
void swap(int *pa, int *pb)
{
    int tmp = *pa;
    *pa = *pb;
    *pb = tmp;
}
int main(void)
{
    int a = 10;
    int b = 20;
    swap(&a, &b);
    printf("a = %d, b = %d\n", a, b);
    return 0;
}

Nous en avons fini avec les pointeurs, du moins, pour le moment. :diable:
En effet, les pointeurs sont omniprésents en langage C et nous n’avons pas fini d’en entendre parler. Mais pour l’heure, nous allons découvrir une des fameuses données complexes dont nous avons parlé en début de chapitre : les structures.

En résumé
  1. Un pointeur est une variable dont le contenu est une adresse ;
  2. L’opérateur d’adressage & permet de récupérer l’adresse d’une variable ;
  3. Un pointeur d’un type peut uniquement contenir l’adresse d’un objet du même type ;
  4. Un pointeur nul contient une adresse invalide qui dépend de votre système d’exploitation ;
  5. Un pointeur nul est obtenu en convertissant zéro vers un type pointeur ou en recourant à la macroconstante NULL .
  6. L’opérateur d’indirection ( * ) permet d’accéder à l’objet référencé par un pointeur ;
  7. En cas de retour d’un pointeur par une fonction, il est impératif de s’assurer que l’objet référencé existe toujours ;
  8. Le type void permet de construire des pointeurs génériques ;
  9. L’indicateur de conversion %p peut être utilisé pour afficher une adresse. Une conversion vers le type void * est pour cela nécessaire.


Les structures - Le langage C • Tutoriels • Zeste de Savoir

Les structures

Définition, initialisation et utilisation

Définition d’une structure

Une structure étant un regroupement d’objets, la première chose à réaliser est la description de celle-ci (techniquement, sa définition), c’est-à-dire préciser de quel(s) objet(s) cette dernière va se composer.

La syntaxe de toute définition est la suivante.

struct étiquette
{
    /* Objet(s) composant(s) la structure. */
};

Prenons un exemple concret : vous souhaitez demander à l’utilisateur deux mesures de temps sous la forme heure(s):minute(s):seconde(s).milliseconde(s) et lui donner la différence entre les deux en secondes. Vous pourriez utiliser six variables pour stocker ce que vous fournit l’utilisateur, toutefois cela reste assez lourd. À la place, nous pourrions représenter chaque mesure à l’aide d’une structure composée de trois objets : un pour les heures, un pour les minutes et un pour les secondes.

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};

Comme vous le voyez, nous avons donné un nom (plus précisément, une étiquette) à notre structure : « temps ». Les règles à respecter sont les mêmes que pour les noms de variable et de fonction.

Pour le reste, la composition de la structure est décrite à l’aide d’une suite de déclarations de variables. Ces différentes déclarations constituent les membres ou champs de la structure. Notez bien qu’il s’agit de déclarations et non de définitions, l’utilisation d’initialisations est donc exclue.

Enfin, notez la présence d’un point-virgule obligatoire à la fin de la définition de la structure.

Une structure ne peut pas comporter plus de cent vingt-sept membres.

Définition d’une variable de type structure

Une fois notre structure décrite, il ne nous reste plus qu’à créer une variable de ce type. Pour ce faire, la syntaxe est la suivante.

struct étiquette identificateur;

La méthode est donc la même que pour définir n’importe quelle variable, si ce n’est que le type de la variable est précisé à l’aide du mot-clé struct et de l’étiquette de la structure. Avec notre exemple de la structure temps , cela donne ceci.

struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};
int main(void)
{
    struct temps t;
    return 0;
}

Initialisation

Comme pour n’importe quelle autre variable, il est possible d’initialiser une variable de type structure dès sa définition. Toutefois, à l’inverse des autres, l’initialisation s’effectue à l’aide d’une liste fournissant une valeur pour un ou plusieurs membres de la structure.

Initialisation séquentielle

L’initialisation séquentielle permet de spécifier une valeur pour un ou plusieurs membres de la structure en suivant l’ordre de la définition. Ainsi, l’exemple ci-dessous initialise le membre heures à 1, minutes à 45 et secondes à 30.560.

struct temps t = { 1, 45, 30.560 };

Initialisation sélective

L’initialisation séquentielle n’est toutefois pas toujours pratique, surtout si les champs que vous souhaitez initialiser sont par exemple au milieu d’une grande structure. Pour éviter ce problème, il est possible de recourir à une initialisation sélective en spécifiant explicitement le ou les champs à initialiser. L’exemple ci-dessous est identique au premier si ce n’est qu’il recourt à une initialisation sélective.

struct temps t = { .secondes = 30.560, .minutes =  45, .heures = 1 };

Notez qu’il n’est plus nécessaire de suivre l’ordre de la définition dans ce cas.

Il est parfaitement possible de mélanger les initialisations séquentielles et sélectives. Dans un tel cas, l’initialisation séquentielle reprend au dernier membre désigné par une initialisation sélective. Partant, le code suivant initialise le membre heures à 1 et le membre secondes à 30.560 .

struct temps t = { 1, .secondes = 30.560 };

Alors que le code ci-dessous initialise le membre minutes à 45 et le membre secondes à 30.560 .

struct temps t = { .minutes = 45, 30.560 };

Dans le cas où vous n’initialisez pas tous les membres de la structure, les autres seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls.

#include <stdio.h>
struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};
int main(void)
{
    struct temps t = { 1 };
    printf("%d, %d, %f\n", t.heures, t.minutes, t.secondes);
    return 0;
}
Résultat
1, 0, 0.000000

Accès à un membre

L’accès à un membre d’une structure se réalise à l’aide de la variable de type structure et de l’opérateur . suivi du nom du champ visé.

variable.membre

Cette syntaxe peut être utilisée aussi bien pour obtenir la valeur d’un champ que pour en modifier le contenu. L’exemple suivant effectue donc la même action que l’initialisation présentée précédemment.

t.heures = 1;
t.minutes = 45;
t.secondes = 30.560;

Exercice

Afin d’assimiler tout ceci, voici un petit exercice.
Essayez de réaliser ce qui a été décrit plus haut : demandez à l’utilisateur de vous fournir deux mesures de temps sous la forme heure(s):minute(s):seconde(s).milliseconde(s) et donnez lui la différence en seconde entre celles-ci.

Voici un exemple d’utilisation.

Première mesure (hh:mm:ss.xxx) : 12:45:50.640
Deuxième mesure (hh:mm:ss.xxx) : 13:30:35.480
Il y 2684.840 seconde(s) de différence.

Correction

Afficher/Masquer le contenu masqué
#include <stdio.h>
#include <stdlib.h>
struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};
int main(void)
{
    struct temps t1;
    struct temps t2;
    printf("Première mesure (hh:mm:ss) : ");
    if (scanf("%u:%u:%lf", &t1.heures, &t1.minutes, &t1.secondes) != 3)
    {
        printf("Mauvaise saisie\n");
        return EXIT_FAILURE;
    }
    printf("Deuxième mesure (hh:mm:ss) : ");
    if (scanf("%u:%u:%lf", &t2.heures, &t2.minutes, &t2.secondes) != 3)
    {
        printf("Mauvaise saisie\n");
        return EXIT_FAILURE;
    }
    t1.minutes += t1.heures * 60;
    t1.secondes += t1.minutes * 60;
    t2.minutes += t2.heures * 60;
    t2.secondes += t2.minutes * 60;
    printf("Il y a %.3f seconde(s) de différence.\n",
    t2.secondes - t1.secondes);
    return 0;
}

Structures et pointeurs

Certains d’entre vous s’en étaient peut-être doutés : s’il existe un objet d’un type, il doit être possible de créer un pointeur vers un objet de ce type. Si oui, sachez que vous aviez raison. :)

struct temps *p;

La définition ci-dessus crée un pointeur p vers un objet de type struct temps .

Accès via un pointeur

L’utilisation d’un pointeur sur structure est un peu plus complexe que celle d’un pointeur vers un type de base. En effet, il y a deux choses à gérer : l’accès via le pointeur et l’accès à un membre. Intuitivement, vous combineriez sans doute les opérateurs * et . comme ceci.

*p.heures = 1;

Toutefois, cette syntaxe ne correspond pas à ce que nous voulons car l’opérateur . s’applique prioritairement à l’opérateur * . Autrement dit, le code ci-dessus accède au champ heures et tente de lui appliquer l’opérateur d’indirection, ce qui est incorrect puisque le membre heures est un entier non signé.

Pour résoudre ce problème, nous devons utiliser des parenthèses afin que l’opérateur . soit appliqué après le déréférencement, ce qui donne la syntaxe suivante.

(*p).heures = 1;

Cette écriture étant un peu lourde, le C fourni un autre opérateur qui combine ces deux opérations : l’opérateur -> .

p->heures = 1;

Le code suivant initialise donc la structure t via le pointeur p .

struct temps t;
struct temps *p = &t;
p->heures = 1;
p->minutes = 45;
p->secondes = 30.560;

Adressage

Il est important de préciser que l’opérateur d’adressage peut s’appliquer aussi bien à une structure qu’à un de ses membres. Ainsi, dans l’exemple ci-dessous, nous définissons un pointeur p pointant sur la structure t et un pointeur q pointant sur le champ heures de la structure t .

struct temps t;
struct temps *p = &t;
int *q = &t.heures;

Pointeurs sur structures et fonctions

Indiquons enfin que l’utilisation de pointeurs est particulièrement propice dans le cas du passage de structures à des fonctions. En effet, rappelez-vous, lorsque vous fournissez un argument lors d’un appel de fonction, la valeur de celui-ci est affectée au paramètre correspondant. Cette règle s’applique également aux structures : la valeur de chacun des membres est copiée une à une. Dès lors, si la structure passée en argument comporte beaucoup de champs, la copie risque d’être longue. L’utilisation d’un pointeur évite ce problème puisque seule la valeur du pointeur (autrement dit, l’adresse vers la structure) sera copiée et non toute la structure.

Portée et déclarations

Portée

Dans les exemples précédents, nous avons toujours placé notre définition de structure en dehors de toute fonction. Cependant, sachez que celle-ci peut être circonscrite à un bloc de sorte de limiter sa portée, comme pour les définitions de variables et les déclarations de variables et fonctions.

Dans l’exemple ci-dessous, la structure temps ne peut être utilisée que dans le bloc de la fonction main() et les éventuels sous-blocs qui la composent.

int main(void)
{
    struct temps {
        unsigned heures;
        unsigned minutes;
        double secondes;
    };
    struct temps t;
    return 0;
}

Notez qu’il est possible de combiner une définition de structure et une définition de variable. En effet, une définition de structure n’étant rien d’autre que la définition d’un nouveau type, celle-ci peut être placée là où est attendu un type dans une définition de variable. Avec l’exemple précédent, cela donne ceci.

int main(void)
{
    struct temps {
        unsigned heures;
        unsigned minutes;
        double secondes;
    } t1;
    struct temps t2;
    return 0;
}

Il y a trois définitions dans ce code : celle du type struct temps , celle de la variable t1 et celle de la variable t2 . Après sa définition, le type struct temps peut tout à fait être utilisé pour définir d’autres variables de ce type, ce qui est le cas de t2 .

Notez qu’il est possible de condenser la définition du type struct temps et de la variable t1 sur une seule ligne, comme ceci.

struct temps { unsigned heures; unsigned minutes; double secondes; } t1;

S’agissant d’une définition de variable, il est également parfaitement possible de l’initialiser.

int main(void)
{
    struct temps {
        unsigned heures;
        unsigned minutes;
        double secondes;
    } t1 = { 1, 45, 30.560 };
    struct temps t2;
    return 0;
}

Enfin, précisons que l’étiquette d’une structure peut être omise lors de sa définition. Néanmoins, cela ne peut avoir lieu que si la définition de la structure est combinée avec une définition de variable. C’est assez logique étant donné qu’il ne peut pas être fait référence à cette définition à l’aide d’une étiquette.

int main(void)
{
    struct {
        unsigned heures;
        unsigned minutes;
        double secondes;
    } t;
    return 0;
}

Déclarations

Jusqu’à présent, nous avons parlé de définitions de structures, toutefois, comme pour les variables et les fonctions, il existe également des déclarations de structures. Une déclaration de structure est en fait une définition sans le corps de la structure.

struct temps;

Quel est l’intérêt de la chose me direz-vous ? Résoudre deux types de problèmes : les structures interdépendantes et les structures comportant un ou des membres qui sont des pointeurs vers elle-même.

Les structures interdépendantes

Deux structures sont interdépendantes lorsque l’une comprend un pointeur vers l’autre et inversement.

struct a {
    struct b *p;
};
struct b {
    struct a *p;
};

Voyez-vous le problème ? Si le type struct b ne peut être utilisé qu’après sa définition, alors le code ci-dessus est faux et le cas de structures interdépendantes est insoluble. Heureusement, il est possible de déclarer le type struct b afin de pouvoir l’utiliser avant sa définition.

Une déclaration de structure crée un type dit incomplet (comme le type void ). Dès lors, il ne peut pas être utilisé pour définir une variable (puisque les membres qui composent la structure sont inconnus). Ceci n’est utilisable que pour définir des pointeurs.

Le code ci-dessous résout le « problème » en déclarant le type struct b avant son utilisation.

struct b;
struct a {
    struct b *p;
};
struct b {
    struct a *p;
};

Nous avons entouré le mot « problème » de guillemets car le premier code que nous vous avons montré n’en pose en fait aucun et compile sans sourciller. :-°

En fait, afin d’éviter ce genre d’écritures, le langage C prévoit l’ajout de déclarations implicites. Ainsi, lorsque le compilateur rencontre un pointeur vers un type de structure qu’il ne connaît pas, il ajoute implicitement une déclaration de cette structure juste avant. Ainsi, le premier et le deuxième code sont équivalents si ce n’est que le premier comporte une déclaration implicite et non une déclaration explicite.

Structure qui pointe sur elle-même

Le deuxième cas où les déclarations de structure s’avèrent nécessaires est celui d’une structure qui comporte un pointeur vers elle-même.

struct a {
    struct a *p;
};

De nouveau, si le type struct a n’est utilisable qu’après sa définition, c’est grillé. Toutefois, comme pour l’exemple précédent, le code est correct, mais pas tout à fait pour les mêmes raisons. Dans ce cas ci, le type struct a est connu car nous sommes en train de le définir. Techniquement, dès que la définition commence, le type est déclaré.

Notez que, comme les définitions, les déclarations (implicites ou non) ont une portée.

Les structures littérales

Nous venons de le voir, manipuler une structure implique de définir une variable. Toutefois, il est des cas où devoir définir une variable est peu utile, par exemple si la structure n’est pas amenée à être modifiée par la suite. Aussi est-il possible de construire des structures dites « littérales », c’est-à-dire sans passer par la définition d’une variable.

La syntaxe est la suivante.

(type) { élément1, élément2, ... }

Où type est le type de la structure et où élément1 et élément2 représentent les éléments d’une liste d’initialisation (comme dans le cas de l’initialisation d’une variable de type structure).

Ces structures littérales peuvent être utilisées partout où une structure est attendue, par exemple comme argument d’un appel de fonction.

#include <stdio.h>
struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};
void affiche_temps(struct temps temps)
{
    printf("%d:%d:%f\n", temps.heures, temps.minutes, temps.secondes);
}
int
main(void)
{
    affiche_temps((struct temps) { .heures = 12, .minutes = 0, .secondes = 1. });
    return 0;
}
Résultat
12:0:1.000000

Comme vous le voyez, au lieu de définir une variable et de passer celle-ci en argument lors de l’appel à la fonction affiche_temps() , nous avons directement fourni une structure littérale.

Classe de stockage

Les structures littérales sont de classe de stockage automatique , elles sont donc détruites à la fin du bloc dans lequel elles ont été créées.

Notez qu’à l’inverse des chaînes de caractères littérales, les structures littérales peuvent parfaitement être modifiées. L’exemple suivant est donc parfaitement correct.

#include <stdio.h>
struct temps {
    unsigned heures;
    unsigned minutes;
    double secondes;
};
int
main(void)
{
    struct temps *p = &(struct temps) { .heures = 12, .minutes = 0, .secondes = 1. };
    p->secondes = 42.; /* Ok. */
    return 0;
}

Un peu de mémoire

Savez-vous comment sont représentées les structures en mémoire ?

Les membres d’une structure sont placés les uns après les autres en mémoire. Par exemple, prenons cette structure.

struct exemple
{
    double flottant;
    char lettre;
    unsigned entier;
};

Si nous supposons qu’un double a une taille de huit octets, un char d’un octet, et un unsigned int de quatre octets, voici ce que devrait donner cette structure en mémoire.

+---------+----------+
| Adresse |  Champ   |
+---------+----------+
|    0    |          |
+---------+          |
|    1    |          |
+---------+          |
|    2    |          |
+---------+          |
|    3    |          |
+---------+ flottant |
|    4    |          |
+---------+          |
|    5    |          |
+---------+          |
|    6    |          |
+---------+          |
|    7    |          |
+---------+----------+
|    8    | lettre   |
+---------+----------+
|    9    |          |
+---------+          |
|    10   |          |
+---------+ entier   |
|    11   |          |
+---------+          |
|    12   |          |
+---------+----------+

Les adresses spécifiées dans le schéma sont fictives et ne servent qu’à titre d’illustration.

L’opérateur sizeof

Voyons à présent comment déterminer cela de manière plus précise, en commençant par la taille des types. L’opérateur sizeof permet de connaître la taille en multiplets ( bytes en anglais) de son opérande. Cet opérande peut être soit un type (qui doit alors être entre parenthèses), soit une expression (auquel cas les parenthèses sont facultatives).

Le résultat de cet opérateur est de type size_t . Il s’agit d’un type entier non signé défini dans l’en-tête <stddef.h> qui est capable de contenir la taille de n’importe quel objet. L’exemple ci-dessous utilise l’opérateur sizeof pour obtenir la taille des types de bases.

Une expression de type size_t peut être affichée à l’aide de l’indicateur de conversion zu .

#include <stdio.h>
int main(void)
{
    double f;
    printf("_Bool : %zu\n", sizeof(_Bool));
    printf("char : %zu\n", sizeof(char));
    printf("short : %zu\n", sizeof(short));
    printf("int : %zu\n", sizeof(int));
    printf("long : %zu\n", sizeof(long));
    printf("float : %zu\n", sizeof(float));
    printf("double : %zu\n", sizeof(double));
    printf("long double : %zu\n", sizeof(long double));
    printf("int : %zu\n", sizeof 5);
    printf("double : %zu\n", sizeof f);
    return 0;
}
Résultat
_Bool : 1
char : 1
short : 2
int : 4
long : 8
float : 4
double : 8
long double : 16
int : 4
double : 8

Le type char a toujours une taille d’un multiplet.

Il est parfaitement possible que vous n’obteniez pas les même valeurs que nous, celles-ci dépendent de votre machine.

Remarquez que les parenthèses ne sont pas obligatoires dans le cas où l’opérande de l’opérateur sizeof est une expression (dans notre exemple : 5 et f ).

Ceci étant dit, voyons à présent ce que donne la taille de la structure présentée plus haut. En toute logique, elle devrait être égale à la somme des tailles de ses membres, chez nous : 13 (8 + 1 + 4).

#include <stddef.h>
#include <stdio.h>
struct exemple
{
    double flottant;
    char lettre;
    unsigned int entier;
};
int main(void)
{
    printf("struct exemple : %zu\n", sizeof(struct exemple));
    return 0;
}
Résultat
struct exemple : 16

Ah ! Il semble que nous avons loupé quelque chose… :-°
Pourquoi obtenons-nous 16 et non 13, comme attendu ? Pour répondre à cette question, nous allons devoir plonger un peu dans les entrailles de notre machine.

Alignement en mémoire

En fait, il faut savoir qu’il n’est dans les faits pas possible de lire ou modifier un multiplet spécifique de la mémoire vive. Techniquement, il est uniquement possible de lire ou écrire des blocs de données de taille fixe appelés mots mémoire. Dès lors, il est nécessaire que les données à lire ou à écrire soient contenues dans un de ces mots mémoires .

Quel rapport avec notre structure me direz-vous ? Supposons que notre RAM utilise des blocs de huit octets. Si notre structure faisait treize octets, voici comment elle serait vue en termes de blocs.

      +---------+----------+
      | Adresse |  Champ   |
  +-> +---------+----------+
  |   |    0    |          |
  |   +---------+          |
  |   |    1    |          |
  |   +---------+          |
  |   |    2    |          |
  |   +---------+          |
  |   |    3    |          |
1 |   +---------+ flottant |
  |   |    4    |          |
  |   +---------+          |
  |   |    5    |          |
  |   +---------+          |
  |   |    6    |          |
  |   +---------+          |
  |   |    7    |          |
  +-> +---------+----------+
  |   |    8    | lettre   |
  |   +---------+----------+
  |   |    9    |          |
  |   +---------+          |
  |   |    10   |          |
  |   +---------+ entier   |
  |   |    11   |          |
2 |   +---------+          |
  |   |    12   |          |
  |   +---------+----------+
  |
  |
  |
  .
  .
  .

Comme vous le voyez, le champ flottant remplit complètement le premier mot, alors que les champs lettre et entiers ne remplissent que partiellement le second. Dans le seul cas de notre structure, cela ne pose pas de problème, sauf que celle-ci n’est pas seule en mémoire. En effet, imaginons maintenant qu’un autre int suive cette structure, nous obtiendrions alors ceci.

      +---------+----------+
      | Adresse |  Champ   |
  +-> +---------+----------+
  |   |    0    |          |
  |   +---------+          |
  |   |    1    |          |
  |   +---------+          |
  |   |    2    |          |
  |   +---------+          |
  |   |    3    |          |
1 |   +---------+ flottant |
  |   |    4    |          |
  |   +---------+          |
  |   |    5    |          |
  |   +---------+          |
  |   |    6    |          |
  |   +---------+          |
  |   |    7    |          |
  +-> +---------+----------+
  |   |    8    | lettre   |
  |   +---------+----------+
  |   |    9    |          |
  |   +---------+          |
  |   |    10   |          |
  |   +---------+ entier   |
  |   |    11   |          |
2 |   +---------+          |
  |   |    12   |          |
  |   +---------+----------+
  |   |    13   |          |
  |   +---------+          |
  |   |    14   |          |
  |   +---------+ int      |
  |   |    15   |          |
  +-> +---------+          |
  |   |    16   |          |
  |   +---------+----------+
  |
  |
3 |
  .
  .
  .

Et là, c’est le drame, le second entier est à cheval sur le deuxième et le troisième mot. Pour éviter ces problèmes, les compilateurs ajoutent des multiplets dit « de bourrage » ( padding en anglais), afin que les données ne soient jamais à cheval sur plusieurs mots. Dans le cas de notre structure, le compilateur a ajouté trois octets de bourrage juste après le membre lettre afin que les deux derniers champs occupent un mot complet.

      +---------+----------+
      | Adresse |  Champ   |
  +-> +---------+----------+
  |   |    0    |          |
  |   +---------+          |
  |   |    1    |          |
  |   +---------+          |
  |   |    2    |          |
  |   +---------+          |
  |   |    3    |          |
1 |   +---------+ flottant |
  |   |    4    |          |
  |   +---------+          |
  |   |    5    |          |
  |   +---------+          |
  |   |    6    |          |
  |   +---------+          |
  |   |    7    |          |
  +-> +---------+----------+
  |   |    8    | lettre   |
  |   +---------+----------+
  |   |    9    |          |
  |   +---------+          |
  |   |    10   | bourrage |
  |   +---------+          |
  |   |    11   |          |
2 |   +---------+----------+
  |   |    12   |          |
  |   +---------+          |
  |   |    13   |          |
  |   +---------+ entier   |
  |   |    14   |          |
  |   +---------+          |
  |   |    15   |          |
  +-> +---------+----------+
  |   |    16   |          |
  |   +---------+          |
  |   |    17   |          |
  |   +---------+ int      |
  |   |    18   |          |
  |   +---------+          |
  |   |    19   |          |
3 |   +---------+----------+
  |
  |
  |
  .
  .
  .

Ainsi, il n’y a plus de problèmes, l’entier suivant la structure n’est plus à cheval sur deux mots.

Mais, comment le compilateur sait-il qu’il doit ajouter des multiplets de bourrage avant le champ entier et précisément trois ?

Cela est dû à la position du champ entier dans la structure. Dans notre exemple, il y a 9 multiplets qui précèdent le champ entier , or le compilateur sait que la taille d’un unsigned int est de quatre multiplets et que la RAM utilise des blocs de huit octets, il en déduit alors qu’il y aura un problème. En effet, pour qu’il n’y ait pas de soucis, il est nécessaire qu’un champ de type unsigned int soit précédé d’un nombre de multiplets qui soit multiple de quatre (d’où les trois multiplets de bourrage pour arriver à 12).

Notez que la même réflexion peut s’opérer en termes d’adresses : dans notre exemple, un champ de type unsigned int doit commencer à une adresse multiple de 4.

Cette contrainte est appelée une contrainte d’alignement (car il est nécessaire d’« aligner » les données en mémoire). Chaque type possède sa propre contrainte d’alignement, dans notre exemple le type unsigned int a une contrainte d’alignement (ou simplement un alignement) de 4.

L’opérateur _Alignof

Il est possible de connaître les contraintes d’alignement d’un type, à l’aide de l’opérateur _Alignof (ou alignof si l’en-tête <stdalign.h> est inclus) 1. Ce dernier s’utilise de la même manière que l’opérateur sizeof , si ce n’est que seul un type peut lui être fourni et non une expression. Le résultat de l’opérateur est, comme pour sizeof , une expression de type size_t .

Le code suivant vous donne les contraintes d’alignement pour chaque type de base.

#include <stdalign.h>
#include <stdio.h>
int main(void)
{
    printf("_Bool: %zu\n", _Alignof(_Bool));
    printf("char: %zu\n", _Alignof(char));
    printf("short: %zu\n", _Alignof(short));
    printf("int : %zu\n", _Alignof(int));
    printf("long : %zu\n", _Alignof(long));
    printf("long long : %zu\n", _Alignof(long long));
    printf("float : %zu\n", alignof(float));
    printf("double : %zu\n", alignof(double));
    printf("long double : %zu\n", alignof(long double));
    return 0;
}
Résultat
_Bool: 1
char: 1
short: 2
int : 4
long : 8
long long : 8
float : 4
double : 8
long double : 16

Le type char ayant une taille d’un multiplet, il peut toujours être contenu dans un mot. Il n’a donc pas de contraintes d’alignement ou, plus précisément, une contrainte d’alignement de 1.

Comme pour l’opérateur sizeof , il est possible que vous n’obteniez pas les même valeurs que nous, celles-ci dépendent de votre machine.

  1. l’opérateur _Alignof a été introduit par la norme C11. Auparavant, pour connaître les contraintes d’alignement, il était nécessaire de se reposer sur les structures et la macrofonction offsetof (définie dans l’en-tête <stddef.h> ).

    ↩

Les structures en elles-mêmes ne sont pas compliquées à comprendre, mais l’intérêt est parfois plus difficile à saisir. Ne vous en faites pas, nous aurons bientôt l’occasion de découvrir des cas où les structures se trouvent être bien pratiques. En attendant, n’hésitez pas à relire le chapitre s’il vous reste des points obscurs.

Continuons notre route et découvrons à présent un deuxième type de données complexes : les tableaux.

En résumé
  1. Une structure est un regroupement de plusieurs objets, potentiellement de types différents ;
  2. La composition d’une structure est précisée à l’aide d’une définition ;
  3. Si certains membres d’une structure ne se voient pas attribuer de valeurs durant l’initialisation d’une variable, ils seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls ;
  4. L’opérateur . permet d’accéder aux champs d’une variable de type structure ;
  5. L’opérateur -> peut être utilisé pour simplifier l’accès aux membres d’une structure via un pointeur ;
  6. Une structure peut être déclarée en omettant sa composition (son corps), le type ainsi déclaré est incomplet jusqu’à ce qu’il soit défini ;
  7. Tous les types ont des contraintes d’alignement, c’est-à-dire un nombre dont les adresses d’un type doivent être le multiple ;
  8. Les contraintes d’alignement peuvent induire l’ajout de multiplets de bourrage entre certains champs d’une structure.


Les tableaux - Le langage C • Tutoriels • Zeste de Savoir

Les tableaux

Les tableaux simples (à une dimension)

Définition

La définition d’un tableau nécessite trois informations :

  • le type des éléments du tableau (rappelez-vous : un tableau est une suite de données de même type ) ;
  • le nom du tableau (en d’autres mots, son identificateur) ;
  • la longueur du tableau (autrement dit, le nombre d’éléments qui le composent). Cette dernière doit être une expression entière .
type identificateur[longueur];

Comme vous le voyez, la syntaxe de la déclaration d’un tableau est similaire à celle d’une variable, la seule différence étant qu’il est nécessaire de préciser le nombre d’éléments entre crochets à la suite de l’identificateur du tableau.

Ainsi, si nous souhaitons par exemple définir un tableau contenant vingt int , nous devons procéder comme suit.

int tab[20];

Initialisation

Comme pour les variables, il est possible d’initialiser un tableau ou, plus précisément, tout ou une partie de ses éléments. L’initialisation se réalise de la même manière que pour les structures, c’est-à-dire à l’aide d’une liste d’initialisation, séquentielle ou sélective.

Initialisation séquentielle

Initialisation avec une longueur explicite

L’initialisation séquentielle permet de spécifier une valeur pour un ou plusieurs membres du tableau en partant du premier élément. Ainsi, l’exemple ci-dessous initialise les trois membres du tableau avec les valeurs 1, 2 et 3.

int tab[3] = { 1, 2, 3 };
Initialisation avec une longueur implicite

Lorsque vous initialisez un tableau, il vous est permis d’omettre la longueur de celui-ci, car le compilateur sera capable d’en déterminer la taille en comptant le nombre d’éléments présents dans la liste d’initialisation. Ainsi, l’exemple ci-dessous est correct et définit un tableau de trois int valant respectivement 1, 2 et 3.

int tab[] = { 1, 2, 3 };

Initialisation sélective

Initialisation avec une longueur explicite

Comme pour les structures, il est par ailleurs possible de désigner spécifiquement les éléments du tableau que vous souhaitez initialiser. Cette initialisation sélective est réalisée à l’aide du numéro du ou des éléments.

Faites attention ! Les éléments sont numérotés à partir de zéro . Nous y viendrons dans la section suivante.

L’exemple ci-dessous définit un tableau de trois int et initialise le troisième élément.

int tab[3] = { [2] = 3 };
Initialisation avec une longueur implicite

Dans le cas où la longueur du tableau n’est pas précisée, le compilateur déduira la taille du tableau du plus grand indice utilisé lors de l’initialisation sélective. Partant, le code ci-dessous créer un tableau de cent int .

int tab[] = { [0] = 42, [1] = 64, [99] = 100 };

Comme pour les structures, dans le cas où vous ne fournissez pas un nombre suffisant de valeurs, les éléments oubliés seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls.

Également, il est possible de mélanger initialisations séquentielles et sélectives. Dans un tel cas, l’initialisation séquentielle reprend au dernier élément désigné par une initialisation sélective. Dès lors, le code ci-dessous définit un tableau de dix int et initialise le neuvième élément à 9 et le dixième à 10.

int tab[] = { [8] = 9, 10 };

Pour un tableau de structures, la liste d’initialisation comportera elle-même une liste d’initialisation pour chaque structure composant le tableau, par exemple comme ceci.

struct temps tab[2] = { { 12, 45, 50.6401 }, { 13, 30, 35.480 } } ;

Notez par ailleurs que, inversement, une structure peut comporter des tableaux comme membres.

Accès aux éléments d’un tableau

L’accès aux éléments d’un tableau se réalise à l’aide d’un indice, un nombre entier correspondant à la position de chaque élément dans le tableau (premier, deuxième, troisième, etc). Cependant, il y a une petite subtilité : les indices commencent toujours à zéro . Ceci tient à une raison historique : les performances étant limitées à l’époque de la conception du langage C, il était impératif d’éviter des calculs inutiles, y compris lors de la compilation 1.

Plus précisément, l’accès aux différents éléments d’un tableau est réalisé à l’aide de l’adresse de son premier élément, à laquelle est ajouté l’indice. En effet, étant donné que tous les éléments ont la même taille et se suivent en mémoire, leurs adresses peuvent se calculer à l’aide de l’adresse du premier élément et d’un décalage par rapport à celle-ci (l’indice, donc).

Or, si le premier indice n’est pas zéro, mais par exemple un, cela signifie que le compilateur doit soustraire une unité à chaque indice lors de la compilation pour retrouver la bonne adresse, ce qui implique des calculs supplémentaires, chose impensable quand les ressources sont limitées.

Prenons un exemple avec un tableau composé de int (ayant une taille de quatre octets) et dont le premier élément est placé à l’adresse 1008. Si vous déterminez à la main les adresses de chaque élément, vous obtiendrez ceci.

Indice | Adresse de l’élément
0 | 1008 (1008 + 0)
1 | 1012 (1008 + 4)
2 | 1016 (1008 + 8)
3 | 1020 (1008 + 12)
4 | 1024 (1008 + 16)
5 | 1028 (1008 + 20)
… | …

En fait, il est possible de reformuler ceci à l’aide d’une multiplication entre l’indice et la taille d’un int .

Indice | Adresse de l’élément
0 | 1008 (1008 + (0 * 4))
1 | 1012 (1008 + (1 * 4))
2 | 1016 (1008 + (2 * 4))
3 | 1020 (1008 + (3 * 4))
4 | 1024 (1008 + (4 * 4))
5 | 1028 (1008 + (5 * 4))
… | …

Nous pouvons désormais formaliser mathématiquement tout ceci en posant T la taille d’un élément du tableau, i l’indice de cet élément, et A l’adresse de début du tableau (l’adresse du premier élément, donc). L’adresse de l’élément d’indice i s’obtient en calculant A + T × i. Ceci étant posé, voyons à présent comment mettre tout cela en œuvre en C.

Le premier élément

Pour commencer, nous avons besoin de l’adresse du premier élément du tableau. Celle-ci s’obtient en fait d’une manière plutôt contre-intuitive : lorsque vous utilisez une variable de type tableau dans une expression, celle-ci est convertie implicitement en un pointeur sur son premier élément. Comme vous pouvez le constater dans l’exemple qui suit, nous pouvons utiliser la variable tab comme nous l’aurions fait s’il s’agissait d’un pointeur.

#include <stdio.h>
int main(void)
{
    int tab[3] = { 1, 2, 3 };
    printf("Premier élément : %d\n", *tab);
    return 0;
}
Résultat
Premier élément : 1

Notez toutefois qu’il n’est pas possible d’affecter une valeur à une variable de type tableau (nous y viendrons bientôt). Ainsi, le code suivant est incorrect.

int t1[3];
int t2[3];
t1 = t2; /* Incorrect */

La règle de conversion implicite comprend néanmoins deux exceptions : l’opérateur & et l’opérateur sizeof .

Lorsqu’il est appliqué à une variable de type tableau, l’opérateur & produit comme résultat l’adresse du premier élément du tableau. Si vous exécutez le code ci-dessous, vous constaterez que les deux expressions donnent un résultat identique.

#include <stdio.h>
int main(void)
{
    int tab[3];
    printf("%p == %p\n", (void *)tab, (void *)&tab);
    return 0;
}

Dans le cas où une expression de type tableau est fournie comme opérande de l’opérateur sizeof , le résultat de celui-ci sera bien la taille totale du tableau (en multiplets) et non la taille d’un pointeur.

#include <stdio.h>
int main(void)
{
    int tab[3];
    int *ptr;
    printf("sizeof tab = %zu\n", sizeof tab);
    printf("sizeof ptr = %zu\n", sizeof ptr);
    return 0;
}
Résultat
sizeof tab = 12
sizeof ptr = 8

Cette propriété vous permet d’obtenir le nombre d’éléments d’un tableau à l’aide de l’expression suivante.

sizeof tab / sizeof tab[0]

Notez que pour obtenir la taille d’un type tableau, la syntaxe est la suivante.

#include <stdio.h>
int main(void)
{
    printf("sizeof int[3] = %zu\n", sizeof(int[3]));
    printf("sizeof double[42] = %zu\n", sizeof(double[42]));
    return 0;
}
Résultat
sizeof int[3] = 12
sizeof double[42] = 336

Les autres éléments

Pour accéder aux autres éléments, il va nous falloir ajouter la position de l’élément voulu à l’adresse du premier élément et ensuite utiliser l’adresse obtenue. Toutefois, recourir à la formule présentée au-dessus ne marchera pas car, en C, les pointeurs sont typés. Dès lors, lorsque vous additionnez un nombre à un pointeur, le compilateur multiplie automatiquement ce nombre par la taille du type d’objet référencé par le pointeur. Ainsi, pour un tableau de int , l’expression tab + 1 est implicitement convertie en tab + sizeof(int) .

Voici un exemple affichant la valeur de tous les éléments d’un tableau.

#include <stdio.h>
int main(void)
{
    int tab[3] = { 1, 2, 3 };
    printf("Premier élément : %d\n", *tab);
    printf("Deuxième élément : %d\n", *(tab + 1));
    printf("Troisième élément : %d\n", *(tab + 2));
    return 0;
}
Résultat
Premier élément : 1
Deuxième élément : 2
Troisième élément : 3

L’expression *(tab + i) étant quelque peu lourde, il existe un opérateur plus concis pour réaliser cette opération : l’opérateur [] . Celui-ci s’utilise de cette manière.

expression[indice]

Ce qui est équivalent à l’expression suivante.

*(expression + indice)

L’exemple suivant est donc identique au précédent.

#include <stdio.h>
int main(void)
{
    int tab[3] = { 1, 2, 3 };
    printf("Premier élément : %d\n", tab[0]);
    printf("Deuxième élément : %d\n", tab[1]);
    printf("Troisième élément : %d\n", tab[2]);
    return 0;
}

Parcours et débordement

Une des erreurs les plus fréquentes en C consiste à dépasser la taille d’un tableau, ce qui est appelé un cas de débordement ( overflow en anglais). En effet, si vous tentez d’accéder à un objet qui ne fait pas partie de votre tableau, vous réalisez un accès mémoire non autorisé, ce qui provoquera un comportement indéfini. Cela arrive généralement lors d’un parcours de tableau à l’aide d’une boucle.

#include <stdio.h>
int main(void)
{
    int tableau[5] = { 784, 5, 45, -12001, 8 };
    int somme = 0;
    for (unsigned i = 0; i <= 5; ++i)
        somme += tableau[i];
    printf("%d\n", somme);
    return 0;
}

Le code ci-dessus est volontairement erroné et tente d’accéder à un élément qui se situe au-delà du tableau. Ceci provient de l’utilisation de l’opérateur <= à la place de l’opérateur < ce qui entraîne un tour de boucle avec i qui est égal à 5, alors que le dernier indice du tableau doit être quatre.

N’oubliez pas : les indices d’un tableau commencent toujours à zéro. En conséquence, les indices valides d’un tableau de n éléments vont de 0 à n - 1.

Tableaux et fonctions

Passage en argument

Étant donné qu’un tableau peut être utilisé comme un pointeur sur son premier élément, lorsque vous passez un tableau en argument d’une fonction, celle-ci reçoit un pointeur vers le premier élément du tableau. Le plus souvent, il vous sera nécessaire de passer également la taille du tableau afin de pouvoir le parcourir.

Le code suivant utilise une fonction pour parcourir un tableau d’entiers et afficher la valeur de chacun de ses éléments.

#include <stdio.h>
void affiche_tableau(int *tab, unsigned taille)
{
    for (unsigned i = 0; i < taille; ++i)
        printf("tab[%u] = %d\n", i, tab[i]);
}
int main(void)
{
    int tab[5] = { 2, 45, 67, 89, 123 };
    affiche_tableau(tab, 5);
    return 0;
}
Résultat
tab[0] = 2
tab[1] = 45
tab[2] = 67
tab[3] = 89
tab[4] = 123

Notez qu’il existe une syntaxe alternative pour déclarer un paramètre de type tableau héritée du langage B (voyez la section suivante).

void affiche_tableau(int tab[], unsigned taille)

Toutefois, nous vous conseillons de recourir à la première écriture, cette dernière étant plus explicite.

Retour de fonction

De la même manière que pour le passage en argument, retourner un tableau revient à retourner un pointeur sur le premier élément de celui-ci. Toutefois, n’oubliez pas les problématiques de classe de stockage ! Si vous retournez un tableau de classe de stockage automatique, vous fournissez à la fonction appelante un pointeur vers un objet qui n’existe plus (puisque l’exécution de la fonction appelée est terminée).

#include <stdio.h>
int *tableau(void)
{
    int tab[5] = { 1, 2, 3, 4, 5 };
    return tab;
}
int main(void)
{
    int *p = tableau(); /* Incorrect */
    printf("%d\n", p[0]);
    return 0;
}

  1. Pour les curieux, vous pouvez lire ce billet (en anglais) qui explique en détail les raisons de ce choix.

    ↩

La vérité sur les tableaux

Nous vous avons dit qu’il n’est pas possible d’affecter une valeur à une variable de type tableau.

int t1[3];
int t2[3];
t1 = t2; /* Incorrect */

Dans cette section, nous allons vous expliquer pourquoi, mais pour cela, il va nous falloir faire un peu d’histoire.

Un peu d’histoire

Le prédécesseur du langage C était le langage B. Lorsque le développement du C a commencé, un des objectifs était de le rendre autant que possible compatible avec le B, afin de ne pas devoir (trop) modifier les codes existants (un code écrit en B pourrait ainsi être compilé avec un compilateur C sans ou avec peu de modifications). Or, en B, un tableau se définissait comme suit.

auto tab[3];

Le langage B était un langage non typé, ce qui explique l’absence de type dans la définition. Le mot-clé auto (toujours présent en langage C, mais devenu obsolète) servait à indiquer que la variable définie était de classe de stockage automatique.

Toutefois, à la différence du langage C, cette définition crée un tableau de trois éléments et un pointeur initialisé avec l’adresse du premier élément. Ainsi, pour créer un pointeur, il suffisait de définir une variable comme un tableau de taille nulle.

auto ptr[];

Le langage C, toujours en gestation, avait repris ce mode de fonctionnement. Cependant, les structures sont arrivées et les problèmes avec. En effet, prenez ce bout de code.

#include <stdio.h>
struct exemple {
    int tab[3];
};
struct exemple exemple_init(void)
{
    struct exemple init = { { 1, 2, 3 } };
    return init;
}
int main(void)
{
    struct exemple s = exemple_init();
    printf("%d\n", s.tab[0]);
    return 0;
}

La fonction exemple_init() retourne une structure qui est utilisée pour initialiser la variable de la fonction main() . Dans un tel cas, comme pour n’importe quelle variable, le contenu de la première structure sera intégralement copié dans la deuxième. Le souci, c’est que si une définition de tableau crée un tableau et un pointeur initialisé avec l’adresse du premier élément de celui-ci, alors il est nécessaire de modifier le champ tab de la structure s lors de la copie sans quoi son champ tab pointera vers le tableau de la structure init (qui n’existera plus puisque de classe de stockage automatique) et non vers le sien. Voilà qui complexifie la copie de structures, particulièrement si sa définition comprend plusieurs tableaux possiblement imbriqués…

Pour contourner ce problème, les concepteurs du langage C ont imaginé une solution (tordue) qui est à l’origine d’une certaine confusion dans l’utilisation des tableaux : une variable de type tableau ne sera plus un pointeur , mais sera convertie en un pointeur sur son premier élément lors de son utilisation .

Conséquences de l’absence d’un pointeur

Étant donné qu’il n’y a plus de pointeur alloué, la copie de structures s’en trouve simplifiée et peut être réalisée sans opération particulière (ce qui était l’objectif recherché).

Toutefois, cela entraîne une autre conséquence : il n’est plus possible d’assigner une valeur à une variable de type tableau, seuls ses éléments peuvent se voir affecter une valeur. Ainsi, le code suivant est incorrect puisqu’il n’y a aucun pointeur pour recevoir l’adresse du premier élément du tableau t2 .

int t1[3];
int t2[3];
t1 = t2; /* Incorrect */

Également, puisqu’une variable de type tableau n’est plus un pointeur, celle-ci n’a pas d’adresse. Dès lors, lorsque l’opérateur & est appliqué à une variable de type tableau, le résultat sera l’adresse du premier élément du tableau puisque seuls les éléments du tableau ont une adresse.

Les tableaux multidimensionnels

Jusqu’à présent, nous avons travaillé avec des tableaux linéaires, c’est-à-dire dont les éléments se suivaient les uns à la suite des autres. Il s’agit de tableaux dit à une dimension ou unidimensionnels.

Cependant, certaines données peuvent être représentées plus simplement sous la forme de tableaux à deux dimensions (autrement dit, organisées en lignes et en colonnes). C’est par exemple le cas des images (non vectorielles) qui sont des matrices de pixels ou, plus simplement, d’une grille de Sudoku qui est organisée en neuf lignes et en neuf colonnes.

Le langage C vous permet de créer et de gérer ce type de tableaux dit multidimensionnels (en fait, des tableaux de tableaux) et ce, bien au-delà de deux dimensions.

Définition

La définition d’un tableau multidimensionnel se réalise de la même manière que celle d’un tableau unidimensionnel si ce n’est que vous devez fournir la taille des différentes dimensions.

Par exemple, si nous souhaitons définir un tableau de int de vingt lignes et trente-cinq colonnes, nous procèderons comme suit.

int tab[20][35];

De même, pour un tableau de double à trois dimensions.

double tab[3][4][5];

Initialisation

Initialisation avec une longueur explicite

L’initialisation d’un tableau multidimensionnel s’effectue à l’aide d’une liste d’initialisation comprenant elle-même des listes d’initialisations.

int t1[2][2] = { { 1, 2 }, { [0] = 3, [1] = 4 } };
int t2[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } };

Initialisation avec une longueur implicite

Lorsque vous initialisez un tableau multidimensionnel, il vous est permis d’omettre la taille de la première dimension . La taille des autres dimensions doit en revanche être spécifiée, le compilateur ne pouvant déduire la taille de toutes les dimensions.

int t1[][2] = { { 1, 2 }, { [0] = 3, [1] = 4 } };
int t2[][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } };

Comme pour les tableaux unidimensionnels, dans le cas où vous ne fournissez pas un nombre suffisant de valeurs, les éléments omis seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls.

Utilisation

Techniquement, un tableau multidimensionnel est un tableau dont les éléments sont eux-mêmes des tableaux. Dès lors, vous avez besoin d’autant d’indices qu’il y a de dimensions. Par exemple, pour un tableau à deux dimensions, vous avez besoin d’un premier indice pour accéder à l’élément souhaité du premier tableau, mais comme cet élément est lui-même un tableau, vous devez utiliser un second indice pour sélectionner un élément de celui-ci. Illustration.

#include <stdio.h>
int main(void)
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };
    printf("tab[0][0] = %d\n", tab[0][0]);
    printf("tab[0][1] = %d\n", tab[0][1]);
    printf("tab[1][0] = %d\n", tab[1][0]);
    printf("tab[1][1] = %d\n", tab[1][1]);
    return 0;
}
Résultat
tab[0][0] = 1
tab[0][1] = 2
tab[1][0] = 3
tab[1][1] = 4

Représentation en mémoire

Techniquement, les données d’un tableau multidimensionnel sont stockées les unes à côté des autres en mémoire : elles sont rassemblées dans un tableau à une seule dimension. Si les langages comme le FORTRAN mémorisent les colonnes les unes après les autres ( column-major order en anglais), le C mémorise les tableaux lignes par lignes ( row-major order ).

          Un exemple de tableau
            à deux dimensions
                                         Lignes par lignes (raw-major order)
            +---+ +---+ +---+   +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
            | 1 | | 2 | | 3 |---> 1 | | 2 | | 3 | | 4 | | 5 | | 6 | | 7 | | 8 | | 9 |
            +---+ +---+ +---+   +---+ +---+ +---+ +-^-+ +---+ +---+ +-^-+ +---+ +---+
            +---+ +---+ +---+                       |                 |
            | 4 | | 5 | | 6 |-----------------------+                 |
            +---+ +---+ +---+                                         |
            +---+ +---+ +---+                                         |
            | 7 | | 8 | | 9 |-----------------------------------------+
            +---+ +---+ +---+
              |     |     |
  +-----------+     |     +-----------+
  |                 |                 |
+-v-+ +---+ +---+ +-v-+ +---+ +---+ +-v-+ +---+ +---+
| 1 | | 4 | | 7 | | 2 | | 5 | | 8 | | 3 | | 6 | | 9 |
+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
      Colonnes par colonnes (column-major order)

Le calcul d’adresse à effectuer est une généralisation du calcul vu au chapitre précédent.

Parcours

Le même exemple peut être réalisé à l’aide de deux boucles imbriquées afin de parcourir le tableau.

#include <stdio.h>
int main(void)
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };
    for (unsigned i = 0; i < 2; ++i)
        for (unsigned j = 0; j < 2; ++j)
            printf("tab[%u][%u] = %d\n", i, j, tab[i][j]);
    return 0;
}

Tableaux multidimensionnels et fonctions

Passage en argument

Souvenez-vous : sauf exceptions, un tableau est converti en un pointeur sur son premier élément. Dès lors, qu’obtenons-nous lors du passage d’un tableau à deux dimensions en argument d’une fonction ? Le premier élément du tableau est un tableau, donc un pointeur sur… Un tableau ( hé oui). :p

La syntaxe d’un pointeur sur tableau est la suivante.

type (*identificateur)[taille];

Vous remarquerez la présence de parenthèses autour du symbole * et de l’identificateur afin de signaler au compilateur qu’il s’agit d’un pointeur sur un tableau et non d’un tableau de pointeurs. Également, notez que la taille du tableau référencé doit être spécifiée. En effet, sans celle-ci, le compilateur ne pourrait pas opérer correctement le calcul d’adresses puisqu’il ne connaîtrait pas la taille des éléments composant le tableau référencé par le pointeur.

La même logique peut être appliquée pour créer des pointeurs sur des tableaux de tableaux.

type (*identificateur)[N][M];

Et ainsi de suite jusqu’à ce que mort s’ensuive… :-°

L’exemple ci-dessous illustre ce qui vient d’être dit en utilisant une fonction pour afficher le contenu d’un tableau à deux dimensions de int .

#include <stdio.h>
void affiche_tableau(int (*tab)[2], unsigned n, unsigned m)
{
    for (unsigned i = 0; i < n; ++i)
        for (unsigned j = 0; j < m; ++j)
            printf("tab[%u][%u] = %d\n", i, j, tab[i][j]);
}
int main(void)
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };
    affiche_tableau(tab, 2, 2);
    return 0;
}

Retour de fonction

Même remarque que pour les tableaux unidimensionnels : attention à la classe de stockage ! Pour le reste, nous vous laissons admirer la syntaxe particulièrement dégoûtante d’une fonction retournant un pointeur sur un tableau de deux int .

#include <stdio.h>
int (*tableau(void))[2] /* Ouh ! Que c'est laid ! */
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };
    return tab;
}
int main(void)
{
    int (*p)[2] = tableau(); /* Incorrect */
    printf("%d\n", p[0][0]);
    return 0;
}

Les tableaux littéraux

Comme pour les structures, il est possible de construire des tableaux « littéraux » c’est-à-dire sans besoin de déclarer une variable. Comme pour les structures, ces tableaux littéraux ont une classe de stockage automatique, ils sont donc détruits à la fin du bloc qui les a vu naître.

La syntaxe est similaire à celle des structures.

(type[taille]) { élément1, élément2 }

Comme pour l’initialisation de n’importe quel tableau, il est possible d’omettre la taille afin que le compilateur la calcule en se basant sur les éléments fournis. L’exemple ci-dessous utilise un tableau littéral en lieu et place d’une variable.

#include <stdio.h>
static void affiche_tableau(int *tab, unsigned taille)
{
    for (unsigned i = 0; i < taille; ++i)
        printf("tab[%u] = %d\n", i, tab[i]);
}
int
main(void)
{
    affiche_tableau((int[]) { 1, 2, 3, 4, 5 }, 5);
    return 0;
}
Résultat
tab[0] = 1
tab[1] = 2
tab[2] = 3
tab[3] = 4
tab[4] = 5

Exercices

Somme des éléments

Réalisez une fonction qui calcule la somme de tous les éléments d’un tableau de int .

Afficher/Masquer le contenu masqué
int somme(int *tab, unsigned taille)
{
    int res = 0 ;
    for (unsigned i = 0; i < taille; ++i)
        res += tableau[i];
    return res;
}

Maximum et minimum

Créez deux fonctions : une qui retourne le plus petit élément d’un tableau de int et une qui renvoie le plus grand élément d’un tableau de int .

Afficher/Masquer le contenu masqué
int minimum(int *tab, unsigned taille)
{
    int min = tab[0];
    for (unsigned i = 1; i < taille; ++i)
        if (tab[i] < min)
            min = tab[i];
    return min;
}
int maximum(int *tab, unsigned taille)
{
    int max = tab[0];
    for (unsigned i = 1; i < taille; ++i)
        if (tab[i] > max)
            max = tab[i];
    return max
}

Recherche d’un élément

Construisez une fonction qui teste la présence d’une valeur dans un tableau de int . Celle-ci retournera 1 si un ou plusieurs éléments du tableau sont égaux à la valeur recherchée, 0 sinon.

Afficher/Masquer le contenu masqué
int find(int *tab, unsigned taille, int val)
{
    for (unsigned i = 0; i < taille; ++i)
        if (tab[i] == val)
            return 1;
    return 0;
}

Inverser un tableau

Produisez une fonction qui inverse le contenu d’un tableau (le premier élément devient le dernier, l’avant-dernier le deuxième et ainsi de suite).

Indice

Afficher/Masquer le contenu masqué

Pensez à la fonction swap() présentée dans le chapitre sur les pointeurs.

Correction

Afficher/Masquer le contenu masqué
void swap(int *pa, int *pb)
{
    int tmp = *pa;
    *pa = *pb;
    *pb = tmp;
}
void invert(int *tab , unsigned taille)
{
    for (unsigned i = 0; i < (taille / 2); ++i)
        swap(tab + i , tab + taille - 1 - i);
    }
}

Produit des lignes

Composez une fonction qui calcule le produit de la somme des éléments de chaque ligne d’un tableau de int à deux dimensions (ce tableau comprend cinq lignes et cinq colonnes).

Afficher/Masquer le contenu masqué e> int produit ( int (*tab)[ 5 ]) { int res = 1 ; for ( unsigned i = 0 ; i < 5 ; ++i) { int tmp = 0 ; for ( unsigned j = 0 ; j < 5 ; ++j) tmp += tab[i][j]; res *= tmp; } return res; }

Triangle de Pascal

Les triangles de Pascal sont des objets mathématiques amusants. Voici une petite animation qui vous expliquera le fonctionnement de ceux-ci.

Explication des triangles de Pascal en image
Explication des triangles de Pascal en image

Votre objectif va être de réaliser un programme qui affiche un triangle de Pascal de la taille souhaitée par l’utilisateur. Pour ce faire, nous allons diviser le triangle en lignes afin de pouvoir le représenter sous la forme d’un tableau à deux dimensions. Chaque élément du tableau se verra attribué soit une valeur du triangle soit zéro pour signaler qu’il n’est pas utilisé.

La première chose que nous allons faire est donc définir un tableau à deux dimensions (nous fixerons la taille des dimensions à dix) dont tous les éléments sont initialisés à zéro. Ensuite, nous allons demander à l’utilisateur d’entrer la taille du triangle qu’il souhaite obtenir (celle-ci ne devra pas être supérieure aux dimensions du tableau).

À présent, passons à la fonction de création du triangle de Pascal. Celle-ci devra mettre en œuvre l’algorithme suivant.

N = taille du triangle de Pascal fournie par l’utilisateur
Mettre la première case du tableau à 1
Pour i = 1, i < N, i = i + 1
    Mettre la première case de la ligne à 1
    Pour j = 1, j < i, j = j + 1
         La case [i,j] prend la valeur [i - 1, j - 1] + [i - 1, j]
    Mettre la dernière case de la ligne à 1

Enfin, il vous faudra écrire une petite fonction pour afficher le tableau ainsi créé.
Bon courage ! ;)

Afficher/Masquer le contenu masqué
#include <stdio.h>
#include <stdlib.h>
void triangle_pascal(int (*tab)[10], unsigned taille)
{
    tab[0][0] = 1;
    for (unsigned i = 1; i < taille; ++i)
    {
        tab[i][0] = 1;
        for (unsigned j = 1; j < i; ++j)
            tab[i][j] = tab[i - 1][j - 1] + tab[i - 1][j];
        tab[i][i] = 1;
    }
}
void print_triangle(int (*tab)[10], unsigned taille)
{
    for (unsigned i = 0; i < taille; ++i)
    {
        for (int sp = taille - 1 - i; sp > 0; --sp)
            printf(" ");
        for (unsigned j = 0; j < taille; ++j)
            if (tab[i][j] != 0)
                printf("%d ", tab[i][j]);
        printf("\n");
    }
}
int main(void)
{
    int tab[10][10] = { { 0 } };
    unsigned taille;
    printf("Taille du triangle: ");
    if (scanf("%u", &taille) != 1)
    {
        printf("Mauvaise saisie\n");
        return EXIT_FAILURE;
    }
    else if (taille > 10)
    {
        printf("La taille ne doit pas être supérieure à 10\n");
        return EXIT_FAILURE;
    }
    triangle_pascal(tab, taille);
    print_triangle(tab, taille);
    return 0;
}

Dans le chapitre suivant, nous aborderons un nouveau type d’agrégat, un peu particulier puisqu’il se base sur les tableaux : les chaînes de caractères.

En résumé
  1. Un tableau est une suite contiguë d’objets de même type ;
  2. La taille d’un tableau est précisée à l’aide d’une expression entière ;
  3. L’accès aux éléments d’un tableau se réalise à l’aide d’un indice ;
  4. Le premier élément d’un tableau a l’indice zéro ;
  5. Il n’est pas possible d’affecter une valeur à une variable de type tableau, seul ses éléments sont modifiables ;
  6. Un tableau est implicitement converti en un pointeur sur son premier élément, sauf lorsqu’il est l’opérande de l’opérateur sizeof ou & .


Les chaînes de caractères - Le langage C • Tutoriels • Zeste de Savoir

Les chaînes de caractères

Qu'est-ce qu'une chaîne de caractères ?

Dans le chapitre sur les variables, nous avions mentionné le type char . Pour rappel, nous vous avions dit que le type char servait surtout au stockage de caractères, mais que comme ces derniers étaient stockés dans l’ordinateur sous forme de nombres, il était également possible d’utiliser ce type pour mémoriser des nombres.

Le seul problème, c’est qu’une variable de type char ne peut stocker qu’une seule lettre, ce qui est insuffisant pour stocker une phrase ou même un mot. Si nous voulons mémoriser un texte, nous avons besoin d’un outil pour rassembler plusieurs lettres dans un seul objet, manipulable dans notre langage. Cela tombe bien, nous en avons justement découvert un au chapitre précédent : les tableaux.

C’est ainsi que le texte est géré en C : sous forme de tableaux de char appelés chaînes de caractères ( strings en anglais).

Représentation en mémoire

Néanmoins, il y a une petite subtilité. Une chaîne de caractères est un peu plus qu’un tableau : c’est un objet à part entière qui doit être manipulable directement. Or, ceci n’est possible que si nous connaissons sa taille.

Avec une taille intégrée

Dans certains langages de programmation, les chaînes de caractères sont stockées sous la forme d’un tableau de char auquel est adjoint un entier pour indiquer sa longueur. Plus précisément, lors de l’allocation du tableau, le compilateur réserve un élément supplémentaire pour conserver la taille de la chaîne. Ainsi, il est aisé de parcourir la chaîne et de savoir quand la fin de celle-ci est atteinte. De telles chaînes de caractères sont souvent appelées des Pascal strings , s’agissant d’une convention apparue avec le langage de programmation Pascal.

Avec une sentinelle

Toutefois, une telle technique limite la taille des chaînes de caractères à la capacité du type entier utilisé pour mémoriser la longueur de la chaîne. Dans la majorité des cas, il s’agit d’un unsigned char , ce qui donne une limite de 255 caractères maximum sur la plupart des machines. Pour ne pas subir cette limitation, les concepteurs du langage C ont adopté une autre solution : la fin de la chaîne de caractères sera indiquée par un caractère spécial, en l’occurrence zéro (noté '\0' ). Les chaînes de caractères qui fonctionnent sur ce principe sont appelées null terminated strings , ou encore C strings .

Cette solution a toutefois deux inconvénients :

  • la taille de chaque chaîne doit être calculée en la parcourant jusqu’au caractère nul ;
  • le programmeur doit s’assurer que chaque chaîne qu’il construit se termine bien par un caractère nul.

Définition, initialisation et utilisation

Définition

Définir une chaîne de caractères, c’est avant tout définir un tableau de char , ce que nous avons vu au chapitre précédent. L’exemple ci-dessous définit un tableau de vingt-cinq char .

char tab[25];

Initialisation

Il existe deux méthodes pour initialiser une chaîne de caractères :

  • de la même manière qu’un tableau ;
  • à l’aide d’une chaîne de caractères littérale.

Avec une liste d’initialisation

Initialisation avec une longueur explicite

Comme pour n’importe quel tableau, l’initialisation se réalise à l’aide d’une liste d’initialisation. L’exemple ci-dessous définit donc un tableau de vingt-cinq char et initialise les sept premiers avec la suite de lettres « Bonjour ».

char chaine[25] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r' };

Étant donné que seule une partie des éléments est initialisée, les autres sont implicitement mis à zéro, ce qui nous donne une chaîne de caractères valide puisqu’elle est bien terminée par un caractère nul. Faites cependant attention à ce qu’il y ait toujours de la place pour un caractère nul.

Initialisation avec une longueur implicite

Dans le cas où vous ne spécifiez pas de taille lors de la définition, il vous faudra ajouter le caractère nul à la fin de la liste d’initialisation pour obtenir une chaîne valide.

char chaine[] = { 'B', 'o', 'n', 'j', 'o', 'u', 'r', '\0' };

Avec une chaîne littérale

Bien que tout à fait valide, cette première solution est toutefois assez fastidieuse. Aussi, il en existe une seconde : recourir à une chaîne de caractères littérale pour initialiser un tableau. Une chaîne de caractères littérale est une suite de caractères entourée par le symbole " . Nous en avons déjà utilisé auparavant comme argument des fonctions printf() et scanf() .

Techniquement, une chaîne littérale est un tableau de char terminé par un caractère nul. Elles peuvent donc s’utiliser comme n’importe quel autre tableau. Si vous exécutez le code ci-dessous, vous remarquerez que l’opérateur sizeof retourne bien le nombre de caractères composant la chaîne littérale (n’oubliez pas de compter le caractère nul) et que l’opérateur [] peut effectivement leur être appliqué.

#include <stdio.h>
int main(void)
{
    printf("%zu\n", sizeof "Bonjour");
    printf("%c\n", "Bonjour"[3]);
    return 0;
}
Résultat
8
j

Ces chaînes de caractères littérales peuvent également être utilisées à la place des listes d’initialisation. En fait, il s’agit de la troisième et dernière exception à la règle de conversion implicite des tableaux.

Initialisation avec une longueur explicite

Dans le cas où vous spécifiez la taille de votre tableau, faites bien attention à ce que celui-ci dispose de suffisamment de place pour accueillir la chaîne entière, c’est-à-dire les caractères qui la composent et le caractère nul.

char chaine[25] = "Bonjour";
Initialisation avec une longueur implicite

L’utilisation d’une chaîne littérale pour initialiser un tableau dont la taille n’est pas spécifiée vous évite de vous soucier du caractère nul puisque celui-ci fait partie de la chaîne littérale.

char chaine[] = "Bonjour";

Utilisation de pointeurs

Nous vous avons dit que les chaînes littérales n’étaient rien d’autre que des tableaux de char terminés par un caractère nul. Dès lors, comme pour n’importe quel tableau, il vous est loisible de les référencer à l’aide de pointeurs.

char *ptr = "Bonjour";

Néanmoins, les chaînes littérales sont des constantes , il vous est donc impossible de les modifier. L’exemple ci-dessous est donc incorrect.

int main(void)
{
    char *ptr = "bonjour";
    ptr[0] = 'B'; /* Incorrect */
    return 0;
}

Notez bien la différence entre les exemples précédents qui initialisent un tableau avec le contenu d’une chaîne littérale (il y a donc copie de la chaîne littérale) et cet exemple qui initialise un pointeur avec l’adresse du premier élément d’une chaîne littérale.

Utilisation

Pour le reste, une chaîne de caractères s’utilise comme n’importe quel autre tableau. Aussi, pour modifier son contenu, il vous faudra accéder à ses éléments un à un.

#include <stdio.h>
int main(void)
{
    char chaine[25] = "Bonjour";
    printf("%s\n", chaine);
    chaine[0] = 'A';
    chaine[1] = 'u';
    chaine[2] = ' ';
    chaine[3] = 'r';
    chaine[4] = 'e';
    chaine[5] = 'v';
    chaine[6] = 'o';
    chaine[7] = 'i';
    chaine[8] = 'r';
    chaine[9] = '\0'; /* N'oubliez pas le caractère nul ! */
    printf("%s\n", chaine);
    return 0;
}
Résultat
Bonjour
Au revoir

Afficher et récupérer une chaîne de caractères

Les chaînes littérales n’étant rien d’autre que des tableaux de char , il vous est possible d’utiliser des chaînes de caractères là où vous employiez des chaînes littérales. Ainsi, les deux exemples ci-dessous afficheront la même chose.

#include <stdio.h>
int main(void)
{
    char chaine[] = "Bonjour\n";
    printf(chaine);
    return 0;
}

#include <stdio.h>
int main(void)
{
    printf("Bonjour\n");
    return 0;
}
Résultat
Bonjour

Toutefois, les fonctions printf() et scanf() disposent d’un indicateur de conversion vous permettant d’afficher ou de demander une chaîne de caractères : s .

Printf

L’exemple suivant illustre l’utilisation de cet indicateur de conversion avec printf() et affiche la même chose que les deux codes précédents.

#include <stdio.h>
int main(void)
{
    char chaine[] = "Bonjour";
    printf("%s\n", chaine);
    return 0;
}
Résultat
Bonjour

Scanf

Le même indicateur de conversion peut être utilisé avec scanf() pour demander à l’utilisateur d’entrer une chaîne de caractères. Cependant, un problème se pose : étant donné que nous devons créer un tableau de taille finie pour accueillir la saisie de l’utilisateur, nous devons impérativement limiter la longueur des données que nous fournit l’utilisateur.

Pour éviter ce problème, il est possible de spécifier une taille maximale à la fonction scanf() . Pour ce faire, il vous suffit de placer un nombre entre le symbole % et l’indicateur de conversion s . L’exemple ci-dessous demande à l’utilisateur d’entrer son prénom (limité à 254 caractères) et affiche ensuite celui-ci.

La fonction scanf() ne décompte pas le caractère nul final de la limite fournie ! Il vous est donc nécessaire de lui indiquer la taille de votre tableau diminuée de un.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char chaine[255];
    printf("Quel est votre prénom ? ");
    if (scanf("%254s", chaine) != 1)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    printf("Bien le bonjour %s !\n", chaine);
    return 0;
}
Résultat
Quel est votre prénom ? Albert
Bien le bonjour Albert !

Chaîne de caractères avec des espaces

Sauf qu’en fait, l’indicateur s signifie : « la plus longue suite de caractère ne comprenant pas d’espaces » (les espaces étant ici entendus comme une suite d’un ou plusieurs des caractères suivants : ' ' , '\f' , '\n' , '\r' , '\t' , '\v' )…

Autrement dit, si vous entrez « Bonjour tout le monde », la fonction scanf() va s’arrêter au mot « Bonjour », ce dernier étant suivi par un espace.

Comment peut-on récupérer une chaîne complète alors ? :euh:

Eh bien, il va falloir nous débrouiller avec l’indicateur c de la fonction scanf() et réaliser nous même une fonction employant ce dernier au sein d’une boucle. Ainsi, nous pouvons par exemple créer une fonction recevant un pointeur sur char et la taille du tableau référencé qui lit un caractère jusqu’à être arrivé à la fin de la ligne ou à la limite du tableau.

Et comment sait-on que la lecture est arrivée à la fin d’une ligne ?

La fin de ligne est indiquée par le caractère \n .
Avec ceci, vous devriez pouvoir construire une fonction adéquate.
Allez, hop , au boulot et faites gaffe aux retours d’erreurs ! :pirate:

#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
bool lire_ligne(char *chaine, size_t max)
{
    for (size_t i = 0; i < max - 1; ++i)
    {
        int c;
        if (scanf("%c", &c) != 1)
            return false;
        else if (c == '\n')
            break;
        chaine[i] = c;
    }
    chaine[i] = '\0';
    return true;
}
int main(void)
{
    char chaine[255];
    printf("Quel est votre prénom ? ");
    if (lire_ligne(chaine, sizeof chaine))
        printf("Bien le bonjour %s !\n", chaine);
    return 0;
}

Quel est votre prénom ? Charles Henri
Bien le bonjour Charles Henri !

Gardez bien cette fonction sous le coude, nous allons en avoir besoin pour la suite. ;)

Lire et écrire depuis et dans une chaîne de caractères

S’il est possible d’afficher et récupérer une chaîne de caractères, il est également possible de lire depuis une chaîne et d’écrire dans une chaîne. À cette fin, deux fonctions qui devraient vous sembler familières existent : snprintf() et sscanf() .

La fonction snprintf

int snprintf(char *chaine, size_t taille, char *format, ...);

Les trois points à la fin du prototype de la fonction signifient que celle-ci attend un nombre variable d’arguments. Nous verrons ce mécanisme plus en détail dans la troisième partie du cours.

La fonction snprintf() est identique à la fonction printf() mis à part que celle-ci écrit les données produites dans une chaîne de caractères au lieu de les afficher à l’écran. Elle écrit au maximum taille - 1 caractères dans la chaîne chaine et ajoute elle-même un caractère nul à la fin. La fonction retourne le nombre de caractères écrit sans compter le caractère nul final ou bien un nombre négatif en cas d’erreur. Toutefois, dans le cas où la chaîne de destination est trop petite pour accueillir toutes les données à écrire, la fonction retourne le nombre de caractères qui aurait dû être écrit (hors caractère nul, à nouveau) si la limite n’avait pas été atteinte.

Cette fonction peut vous permettre, entre autres, d’écrire un nombre dans une chaîne de caractères.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char chaine[8];
    unsigned long long n = 64;
    if (snprintf(chaine, sizeof chaine, "%llu", n) < 0)
    {
        printf("Erreur lors de la conversion\n");
        return EXIT_FAILURE;
    }
    printf("chaine : %s\n", chaine);
    n = 18446744073709551615LLU;
    int ret = snprintf(chaine, sizeof chaine, "%llu", n);
    if (ret < 0)
    {
        printf("Erreur lors de la conversion\n");
        return EXIT_FAILURE;
    }
    if (ret + 1 > sizeof chaine) // snprintf ne compte pas le caractère nul
        printf("La chaîne est trop petite, elle devrait avoir une taille de : %d\n", ret + 1);
    printf("chaine : %s\n", chaine);
    return 0;
}
Résultat
chaine : 64
La chaîne est trop petite, elle devrait avoir une taille de : 21
chaine : 1844674

Comme vous le voyez, dans le cas où il n’y a pas assez d’espace, la fonction snprintf() retourne bien le nombre de caractères qui auraient dû être écrit (ici 20, caractère nul exclu). Cette propriété peut d’ailleurs être utilisée pour calculer la taille nécessaire d’une chaîne à l’avance en précisant une taille de zéro. Dans un tel cas, aucun caractère ne sera écrit, un pointeur nul peut alors être passé en argument.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    unsigned long long n = 18446744073709551615LLU;
    int ret = snprintf(NULL, 0, "%llu", n);
    if (ret < 0)
    {
        printf("Erreur lors de la conversion\n");
        return EXIT_FAILURE;
    }
    else
        printf("Il vous faut une chaîne de %d caractère(s)\n", ret + 1);
    return 0;
}
Résultat
Il vous faut une chaîne de 21 caractère(s)

La fonction sscanf

int sscanf(char *chaine, char *format, ...);

La fonction sscanf() est identique à la fonction scanf() si ce n’est que celle-ci extrait les données depuis une chaîne de caractères plutôt qu’en provenance d’une saisie de l’utilisateur. Cette dernière retourne le nombre de conversions réussies ou un nombre inférieur si elles n’ont pas toutes été réalisées ou enfin un nombre négatif en cas d’erreur.

Voici un exemple d’utilisation.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char chaine[10];
    int n;
    if (sscanf("5 abcd", "%d %9s", &n, chaine) != 2)
    {
        printf("Erreur lors de l'examen de la chaîne\n");
        return EXIT_FAILURE;
    }
    printf("%d %s\n", n, chaine);
    return 0;
}
Résultat
5 abcd

Les classes de caractères

Pour terminer cette partie théorique sur une note un peu plus légère, sachez que la bibliothèque standard fournit un en-tête <ctype.h> qui permet de classifier les caractères. Onze fonctions sont ainsi définies.

int isalnum(int c);
int isalpha(int c);
int isblank(int c);
int iscntrl(int c);
int isdigit(int c);
int isgraph(int c);
int islower(int c);
int isprint(int c);
int ispunct(int c);
int isspace(int c);
int isupper(int c);
int isxdigit(int c);

Chacune d’entre elles attend en argument un caractère et retourne un nombre positif ou zéro suivant que le caractère fourni appartienne ou non à la catégorie déterminée par la fonction.

Fonction | Catégorie | Description | Par défaut
isupper | Majuscule | Détermine si le caractère entré est une lettre majuscule | 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'G' , 'H' , 'I' , 'J' , 'K' , 'L' , 'M' , 'N' , 'O' , 'P' , 'Q' , 'R' , 'S' , 'T' , 'U' , 'V' , 'W' , 'X' , 'Y' ou 'Z'
islower | Minuscule | Détermine si le caractère entré est une lettre minuscule | 'a' , 'b' , 'c' , 'd' , 'e' , 'f' , 'g' , 'h' , 'i' , 'j' , 'k' , 'l' , 'm' , 'o' , 'n' , 'p' , 'q' , 'r' , 's' , 't' , 'u' , 'v' , 'w' , 'x' , 'y' ou 'z'
isdigit | Chiffre décimal | Détermine si le caractère entré est un chiffre décimal | '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' ou '9'
isxdigit | Chiffre hexadécimal | Détermine si le caractère entré est un chiffre hexadécimal | '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'A' , 'B' , 'C' , 'D' , 'E' , 'F' , 'a' , 'b' , 'c' , 'd' , 'e' ou 'f'
isblank | Espace | Détermine si le caractère entré est un espacement blanc | ' ' ou '\t'
isspace | Espace | Détermine si le caractère entré est un espace | ' ' , '\f' , '\n' , '\r' , '\t' ou '\v'
iscntrl | Contrôle | Détermine si le caractère est un caractère dit « de contrôle » | '\0' , '\a' , '\b' , '\f' , '\n' , '\r' , '\t' ou '\v'
ispunct | Ponctuation | Détermine si le caractère entré est un signe de ponctuation | '!' , '"' , '#' , '%' , '&' , ''' , '(' , ')' , '*' , '+' , ',' , '-' , '.' , '/' , ':' , ';' , '<' , '=' , '>' , '?' , '[' , '\' , ']' , '^' , '_' , '{' , '<barre droite>' , '}' ou '~'
isalpha | Alphabétique | Détermine si le caractère entré est une lettre alphabétique | Les deux ensembles de caractères de islower() et isupper()
isalnum | Alphanumérique | Détermine si le caractère entré est une lettre alphabétique ou un chiffre décimal | Les trois ensembles de caractères de islower() , isupper() et isdigit()
isgraph | Graphique | Détermine si le caractère est représentable graphiquement | Tout sauf l’ensemble de iscntrl() et l’espace ( ' ' )
isprint | Affichable | Détermine si le caractère est « affichable » | Tout sauf l’ensemble de iscntrl()

La suite <barre_droite> symbolise le caractère | .

Le tableau ci-dessus vous présente chacune de ces onze fonctions ainsi que les ensembles de caractères qu’elles décrivent. La colonne « par défaut » vous détaille leur comportement en cas d’utilisation de la locale C . Nous reviendrons sur les locales dans la troisième partie de ce cours ; pour l’heure, considérez que ces fonctions ne retournent un nombre positif que si un des caractères de leur ensemble « par défaut » leur est fourni en argument.

L’exemple ci-dessous utilise la fonction isdigit() pour déterminer si l’utilisateur a bien entré une suite de chiffres.

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
/* lire_ligne() */
int main(void)
{
    char suite[255];
    if (!lire_ligne(suite, sizeof suite))
    {
        printf("Erreur lors de la saisie.\n");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; suite[i] != '\0'; ++i)
        if (!isdigit(suite[i]))
        {
            printf("Veuillez entrer une suite de chiffres.\n");
            return EXIT_FAILURE;
        }
    printf("C'est bien une suite de chiffres.\n");
    return 0;
}
Résultat
122334
C'est bien une suite de chiffres.
5678a
Veuillez entrer une suite de chiffres.

Notez que cet en-tête fournit également deux fonctions : tolower() et toupper() retournant respectivement la version minuscule ou majuscule de la lettre entrée. Dans le cas où un caractère autre qu’une lettre est entrée (ou que celle-ci est déjà en minuscule ou en majuscule), la fonction retourne celui-ci.

Exercices

Palindrome

Un palindrome est un texte identique lorsqu’il est lu de gauche à droite et de droite à gauche. Ainsi, le mot radar est un palindrome, de même que les phrases engage le jeu que je le gagne et élu par cette crapule . Normalement, il n’est pas tenu compte des accents, trémas, cédilles ou des espaces. Toutefois, pour cet exercice, nous nous contenterons de vérifier si un mot donné est un palindrome.

Afficher/Masquer le contenu masqué e>
bool palindrome(char *s)
{
    size_t len = 0;
    while (s[len] != '\0')
        ++len;
    for (size_t i = 0; i < len; ++i)
        if (s[i] != s[len - 1 - i])
            return false;
    return true;
}

Compter les parenthèses

Écrivez un programme qui lit une ligne et vérifie que chaque parenthèse ouvrante est bien refermée par la suite.

Entrez une ligne : printf("%zu\n", sizeof(int);
Il manque des parenthèses.
Afficher/Masquer le contenu masqué
#include <stdio.h>
#include <stdlib.h>
/* lire_ligne() */
int main(void)
{
    char s[255];
    printf("Entrez une ligne : ");
    if (!lire_ligne(s, sizeof s))
    {
        printf("Erreur lors de la saisie.\n");
        return EXIT_FAILURE;
    }
    int n = 0;
    for (char *t = s; *t != '\0'; ++t)
        if (*t == '(')
            ++n;
        else if (*t == ')')
            --n;
    if (n == 0)
        printf("Le compte est bon.\n");
    else
        printf("Il manque des parenthèses.\n");
    return 0;
}

Le chapitre suivant sera l’occasion de mettre en pratique ce que nous avons vu dans les chapitres précédents à l’aide d’un TP.

En résumé
  1. Une chaîne de caractères est un tableau de char terminé par un élément nul ;
  2. Une chaîne littérale ne peut pas être modifiée ;
  3. L’indicateur de conversion s permet d’afficher ou de récupérer une chaîne de caractères ;
  4. Les fonctions sscanf() et snprintf() permet de lire ou d’écrire depuis et dans une chaîne de caractères ;
  5. Les fonctions de l’en-tête <ctype.h> permettent de classifier les caractères.


TP : l'en-tête <string.h> - Le langage C • Tutoriels • Zeste de Savoir

TP : l'en-tête <string.h>

Préparation

Dans le chapitre précédent, nous vous avons dit qu’une chaîne de caractères se manipulait comme un tableau, à savoir en parcourant ses éléments un à un. Cependant, si cela s’arrêtait là, cela serait assez gênant. En effet, la moindre opération sur une chaîne nécessiterait d’accéder à ses différents éléments, que ce soit une modification, une copie, une comparaison, etc. Heureusement pour nous, la bibliothèque standard fournit une suite de fonctions nous permettant de réaliser plusieurs opérations de base sur des chaînes de caractères. Ces fonctions sont déclarées dans l’en-tête <string.h> .

L’objectif de ce TP sera de réaliser une version pour chacune des fonctions de cet en-tête qui vont vous êtes présentées. :)

strlen

size_t strlen(char *chaine);

La fonction strlen() vous permet de connaître la taille d’une chaîne fournie en argument. Celle-ci retourne une valeur de type size_t . Notez bien que la longueur retournée ne comprend pas le caractère nul. L’exemple ci-dessous affiche la taille de la chaîne « Bonjour ».

#include <stdio.h>
#include <string.h>
int main(void)
{
    printf("Longueur : %zu\n", strlen("Bonjour"));
    return 0;
}
Résultat
Longueur : 7

strcpy

char *strcpy(char *destination, char *source);

La fonction strcpy() copie le contenu de la chaîne source dans la chaîne destination , caractère nul compris. La fonction retourne l’adresse de destination . L’exemple ci-dessous copie la chaîne « Au revoir » dans la chaîne chaine .

#include <stdio.h>
int main(void)
{
    char chaine[25] = "Bonjour\n";
    strcpy(chaine, "Au revoir");
    printf("%s\n", chaine);
    return 0;
}
Résultat
Au revoir

La fonction strcpy() n’effectue aucune vérification. Vous devez donc vous assurer que la chaîne de destination dispose de suffisamment d’espace pour accueillir la chaîne qui doit être copiée (caractère nul compris !).

strcat

char *strcat(char *destination, char *source);

La fonction strcat() ajoute le contenu de la chaîne source à celui de la chaîne destination , caractère nul compris. La fonction retourne l’adresse de destination . L’exemple ci-dessous ajoute la chaîne « tout le monde » au contenu de la chaîne chaine .

#include <stdio.h>
#include <string.h>
int main(void)
{
    char chaine[25] = "Bonjour";
    strcat(chaine, " tout le monde");
    printf("%s\n", chaine);
    return 0;
}
Résultat
Bonjour tout le monde

Comme strcpy() , la fonction strcat() n’effectue aucune vérification. Vous devez donc vous assurer que la chaîne de destination dispose de suffisamment d’espace pour accueillir la chaîne qui doit être ajoutée (caractère nul compris !).

strcmp

int strcmp(char *chaine1, char *chaine2);

La fonction strcmp() compare deux chaînes de caractères. Cette fonction retourne :

  • une valeur positive si la première chaîne est « plus grande » que la seconde ;
  • zéro si elles sont égales ;
  • une valeur négative si la seconde chaîne est « plus grande » que la première.

Ne vous inquiétez pas au sujet des valeurs positives et négatives, nous y reviendrons en temps voulu lorsque nous aborderons les notions de jeux de caractères et d’encodages dans la troisième partie du cours. En attendant, effectuez simplement une comparaison entre les deux caractères qui diffèrent.

#include <stdio.h>
#include <string.h>
int main(void)
{
    char chaine1[] = "Bonjour";
    char chaine2[] = "Au revoir";
    if (strcmp(chaine1, chaine2) == 0)
        printf("Les deux chaînes sont identiques\n");
    else
        printf("Les deux chaînes sont différentes\n");
    return 0;
}
Résultat
Les deux chaînes sont différentes

strchr

char *strchr(char *chaine, int ch);

La fonction strchr() recherche la présence du caractère ch dans la chaîne chaine . Si celui-ci est rencontré, la fonction retourne l’adresse de la première occurrence de celui-ci au sein de la chaîne. Dans le cas contraire, la fonction renvoie un pointeur nul.

#include <stddef.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
    char chaine[] = "Bonjour";
    char *p = strchr(chaine, 'o');
    if (p != NULL)
        printf("La chaîne `%s' contient la lettre %c\n", chaine, *p);
    return 0;
}
Résultat
La chaîne `Bonjour' contient la lettre o

strpbrk

char *strpbrk(char *chaine, char *liste);

La fonction strpbrk() recherche la présence d’un des caractères de la chaîne liste dans la chaîne chaine . Si un de ceux-ci est rencontré, la fonction retourne l’adresse de la première occurrence au sein de la chaîne. Dans le cas contraire, la fonction renvoie un pointeur nul.

#include <stddef.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
    char chaine[] = "Bonjour";
    char *p = strpbrk(chaine, "aeiouy");
    if (p != NULL)
        printf("La première voyelle de la chaîne `%s' est : %c\n", chaine, *p);
    return 0;
}
Résultat
La première voyelle de la chaîne `Bonjour' est : o

strstr

char *strstr(char *chaine1, char *chaine2);

La fonction strstr() recherche la présence de la chaîne chaine2 dans la chaîne chaine1 . Si celle-ci est rencontrée, la fonction retourne l’adresse de la première occurrence de celle-ci au sein de la chaîne. Dans le cas contraire, la fonction renvoie un pointeur nul.

#include <stddef.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
    char chaine[] = "Bonjour";
    char *p = strstr(chaine, "jour");
    if (p != NULL)
        printf("La chaîne `%s' contient la chaîne `%s'\n", chaine, p);
    return 0;
}
Résultat
La chaîne `Bonjour' contient la chaîne `jour'

Cela étant dit, à vous de jouer. ;)

Par convention, nous commencerons le nom de nos fonctions par la lettre « x » afin d’éviter des collisions avec ceux de l’en-tête <string.h> .

Correction

Bien, l’heure de la correction est venue. :pirate:

strlen

size_t xstrlen(char *chaine)
{
    for (size_t i = 0; chaine[i] != '\0'; ++i)
        ;
    return i;
}

strcpy

char *xstrcpy(char *destination, char *source)
{
    size_t i;
    for (i = 0; source[i] != '\0'; ++i)
        destination[i] = source[i];
    destination[i] = '\0' // N'oubliez pas le caractère nul final !
    return destination;
}

strcat

char *xstrcat(char *destination, char *source)
{
    while (*destination != '\0')
        ++destination;
    size_t i;
    for (i = 0; source[i] != '\0'; ++i)
        destination[i] = source[i];
    destination[i] = '\0'; // N'oubliez pas le caractère nul final !
    return destination;
}

strcmp

int xstrcmp(char *chaine1, char *chaine2)
{
    while (*chaine1 == *chaine2)
    {
        if (*chaine1 == '\0')
            return 0;
        ++chaine1;
        ++chaine2;
    }
    return (*chaine1 < *chaine2) ? -1 : 1;
}

strchr

char *xstrchr(char *chaine, int ch)
{
    while (*chaine != '\0')
        if (*chaine == ch)
            return chaine;
        else
            ++chaine;
    return NULL;
}

strpbrk

char *xstrpbrk(char *chaine, char *liste)
{
    while (*chaine != '\0')
    {
        char *p = liste;
        while (*p != '\0')
        {
            if (*chaine == *p)
                return chaine;
            ++p;
        }
        ++chaine;
    }
    return NULL;
}

strstr

char *xstrstr(char *chaine1, char *chaine2)
{
    while (*chaine1 != '\0')
    {
        char *s = chaine1;
        char *t = chaine2;
        while (*s != '\0' && *t != '\0')
        {
            if (*s != *t)
                break;
            ++s;
            ++t;
        }
        if (*t == '\0')
            return chaine1;
        ++chaine1;
    }
    return NULL;
}

Pour aller plus loin : strtok

Pour les plus aventureux d’entre-vous, nous vous proposons de réaliser en plus la mise en œuvre de la fonction strtok() qui est un peu plus complexe que ses congénères. :)

char *strtok(char *chaine, char *liste);

La fonction strtok() est un peu particulière : elle divise la chaîne chaine en une suite de sous-chaînes délimitée par un des caractères présents dans la chaîne liste . En fait, cette dernière remplace les caractères de la chaîne liste par des caractères nuls dans la chaîne chaine (elle modifie donc la chaîne chaine !) et renvoie les différentes sous-chaînes ainsi créées au fur et à mesure de ses appels.

Lors des appels subséquents, la chaîne chaine doit être un pointeur nul afin de signaler à strtok() que nous souhaitons la sous-chaîne suivante. S’il n’y a plus de sous-chaîne ou qu’aucun caractère de la chaîne liste n’est trouvé, celle-ci retourne un pointeur nul.

L’exemple ci-dessous tente de scinder la chaîne « un, deux, trois » en trois sous-chaînes.

#include <stddef.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
    char chaine[] = "un, deux, trois";
    char *p = strtok(chaine, ", ");
    for (size_t i = 0; p != NULL; ++i)
    {
        printf("%u : %s\n", i, p);
        p = strtok(NULL, ", ");
    }
    return 0;
}
Résultat
0 : un
1 : deux
2 : trois

La fonction strtok() étant un peu plus complexe que les autres, voici une petite marche à suivre pour réaliser cette fonction.

  • regarder si la chaîne fournie (ou la sous-chaîne suivante) contient, à son début , des caractères présents dans la seconde chaîne. Si oui, ceux-ci doivent être passés ;
  • si la fin de la (sous-)chaîne est atteinte, retourner un pointeur nul ;
  • parcourir la chaîne fournie (ou la sous-chaîne courante) jusqu’à rencontrer un des caractères composant la seconde chaîne. Si un caractère est rencontré, le remplacer par un caractère nul, conserver la position actuelle dans la chaîne et retourner la sous-chaîne ainsi créée ( sans les éventuels caractères passés au début) ;
  • si la fin de la (sous-)chaîne est atteinte, retournez-la (sous-)chaîne ( sans les éventuels caractères passés au début).

Vous aurez besoin d’une variable de classe de stockage statique pour réaliser cette fonction. Également, l’instruction goto pourra vous être utile.

Correction

char *xstrtok(char *chaine, char *liste)
{
    static char *dernier;
    char *base = (chaine != NULL) ? chaine : dernier;
    if (base == NULL)
        return NULL;
separateur_au_debut:
    for (char *sep = liste; *sep != '\0'; ++sep)
        if (*base == *sep)
        {
            ++base;
            goto separateur_au_debut;
        }
    if (*base == '\0')
    {
        dernier = NULL;
        return NULL;
    }
    for (char *s = base; *s != '\0'; ++s)
        for (char *sep = liste; *sep != '\0'; ++sep)
            if (*s == *sep)
            {
                *s = '\0';
                dernier = s + 1;
                return base;
            }
    dernier = NULL;
    return base;
}

Dans le chapitre suivant, nous aborderons le mécanisme de l’allocation dynamique de mémoire.


L'allocation dynamique - Le langage C • Tutoriels • Zeste de Savoir

L'allocation dynamique

La notion d'objet

Jusqu’à présent, nous avons toujours recouru au système de types et de variables du langage C pour stocker nos données sans jamais vraiment nous soucier de ce qui se passait « en dessous ». Il est à présent temps de lever une partie de ce voile en abordant la notion d’objet.

En C, un objet est une zone mémoire pouvant contenir des données et est composée d’une suite contiguë d’un ou plusieurs multiplets. En fait, tous les types du langage C manipulent des objets. La différence entre les types tient simplement en la manière dont ils répartissent les données au sein de ces objets, ce qui est appelé leur représentation (celle-ci sera abordée dans la troisième partie du cours). Ainsi, la valeur 1 n’est pas représentée de la même manière dans un objet de type int que dans un objet de type double .

Un objet étant une suite contiguë de multiplets, il est possible d’en examiner le contenu en lisant ses multiplets un à un. Ceci peut se réaliser en C à l’aide de l’adresse de l’objet et d’un pointeur sur unsigned char , le type char du C ayant toujours la taille d’un multiplet. Notez qu’il est impératif d’utiliser la version non signée du type char afin d’éviter des problèmes de conversions.

L’exemple ci-dessous affiche les multiplets composant un objet de type int et un objet de type double en hexadécimal.

#include <stdio.h>
int main(void)
{
    int n = 1;
    double f = 1.;
    unsigned char *byte = (unsigned char *)&n;
    for (unsigned i = 0; i < sizeof n; ++i)
        printf("%x ", byte[i]);
    printf("\n");
    byte = (unsigned char *)&f;
    for (unsigned i = 0; i < sizeof f; ++i)
        printf("%x ", byte[i]);
    printf("\n");
    return 0;
}
Résultat
1 0 0 0
0 0 0 0 0 0 f0 3f

Il se peut que vous n’obteniez pas le même résultat que nous, ce dernier dépend de votre machine.

Comme vous le voyez, la représentation de la valeur 1 n’est pas du tout la même entre le type int et le type double .

Malloc et consoeurs

La bibliothèque standard fournit trois fonctions vous permettant d’allouer de la mémoire : malloc() , calloc() et realloc() et une vous permettant de la libérer : free() . Ces quatre fonctions sont déclarées dans l’en-tête <stdlib.h> .

malloc

void *malloc(size_t taille);

La fonction malloc() vous permet d’allouer un objet de la taille fournie en argument (qui représente un nombre de multiplets) et retourne l’adresse de cet objet sous la forme d’un pointeur générique. En cas d’échec de l’allocation, elle retourne un pointeur nul.

Vous devez toujours vérifier le retour d’une fonction d’allocation afin de vous assurer que vous manipulez bien un pointeur valide.

Allocation d’un objet

Dans l’exemple ci-dessous, nous réservons un objet de la taille d’un int , nous y stockons ensuite la valeur dix et l’affichons. Pour cela, nous utilisons un pointeur sur int qui va se voir affecter l’adresse de l’objet ainsi alloué et qui va nous permettre de le manipuler comme nous le ferions s’il référençait une variable de type int .

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int));
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    *p = 10;
    printf("%d\n", *p);
    return 0;
}
Résultat
10

Allocation d’un tableau

Pour allouer un tableau, vous devez réserver un bloc mémoire de la taille d’un élément multiplié par le nombre d’éléments composant le tableau. L’exemple suivant alloue un tableau de dix int , l’initialise et affiche son contenu.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int) * 10);
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; i < 10; ++i)
    {
        p[i] = i * 10;
        printf("p[%u] = %d\n", i, p[i]);
    }
    return 0;
}
Résultat
p[0] = 0
p[1] = 10
p[2] = 20
p[3] = 30
p[4] = 40
p[5] = 50
p[6] = 60
p[7] = 70
p[8] = 80
p[9] = 90

Autrement dit et de manière plus générale : pour allouer dynamiquement un objet de type T , il vous faut créer un pointeur sur le type T qui conservera son adresse.

Notez qu’il est possible de simplifier l’expression fournie à malloc() en écrivant sizeof(int[10]) qui a le mérite d’être plus concise et plus claire.

La fonction malloc() n’effectue aucune initialisation, le contenu du bloc alloué est donc indéterminé.

free

void free(void *ptr);

La fonction free() libère le bloc précédemment alloué par une fonction d’allocation dont l’adresse est fournie en argument. Dans le cas où un pointeur nul lui est fourni, elle n’effectue aucune opération.

Retenez bien la règle suivante : à chaque appel à une fonction d’allocation doit correspondre un appel à la fonction free() .

Dès lors, nous pouvons compléter les exemples précédents comme suit.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int));
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    *p = 10;
    printf("%d\n", *p);
    free(p);
    return 0;
}

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int[10]));
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; i < 10; ++i)
    {
        p[i] = i * 10;
        printf("p[%u] = %d\n", i, p[i]);
    }
    free(p);
    return 0;
}

Remarquez que même si le deuxième exemple alloue un tableau, il n’y a bien eu qu’ une seule allocation. Un seul appel à la fonction free() est donc nécessaire.

calloc

void *calloc(size_t nombre, size_t taille);

La fonction calloc() attend deux arguments : le nombre d’éléments à allouer et la taille de chacun de ces éléments. Techniquement, elle revient au même que d’appeler malloc() comme suit.

malloc(nombre * taille);

à un détail près : chaque multiplet de l’objet ainsi alloué est initialisé à zéro .

Faites attention : cette initialisation n’est pas similaire à celle des variables de classe de stockage statique ! En effet, le fait que chaque multiplet ait pour valeur zéro ne signifie pas qu’il s’agit de la représentation du zéro dans le type donné. Si cela est par exemple vrai pour les types entiers, ce n’est pas le cas pour les pointeurs (rappelez-vous : un pointeur nul référence une adresse invalide qui dépend de votre système) ou pour les nombres réels (nous y reviendrons plus tard). De manière générale, ne considérez cette initialisation utile que pour les entiers et les chaînes de caractères.

realloc

void *realloc(void *p, size_t taille);

La fonction realloc() libère un bloc de mémoire précédemment alloué, en réserve un nouveau de la taille demandée et copie le contenu de l’ancien objet dans le nouveau. Dans le cas où la taille demandée est inférieure à celle du bloc d’origine, le contenu de celui-ci sera copié à hauteur de la nouvelle taille. À l’inverse, si la nouvelle taille est supérieure à l’ancienne, l’excédent n’est pas initialisé.

La fonction attend deux arguments : l’adresse d’un bloc précédemment alloué à l’aide d’une fonction d’allocation et la taille du nouveau bloc à allouer. Elle retourne l’adresse du nouveau bloc ou un pointeur nul en cas d’erreur.

L’exemple ci-dessous alloue un tableau de dix int et utilise realloc() pour agrandir celui-ci à vingt int .

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int[10]));
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; i < 10; ++i)
        p[i] = i * 10;
    int *tmp = realloc(p, sizeof(int[20]));
    if (tmp == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    p = tmp;
    for (unsigned i = 10; i < 20; ++i)
        p[i] = i * 10;
    for (unsigned i = 0; i < 20; ++i)
        printf("p[%u] = %d\n", i, p[i]);
    free(p);
    return 0;
}
Résultat
p[0] = 0
p[1] = 10
p[2] = 20
p[3] = 30
p[4] = 40
p[5] = 50
p[6] = 60
p[7] = 70
p[8] = 80
p[9] = 90
p[10] = 100
p[11] = 110
p[12] = 120
p[13] = 130
p[14] = 140
p[15] = 150
p[16] = 160
p[17] = 170
p[18] = 180
p[19] = 190

Remarquez que nous avons utilisé une autre variable, tmp , pour vérifier le retour de la fonction realloc() . En effet, si nous avions procéder comme ceci.

p = realloc(p, sizeof(int[20]));

Il nous aurait été impossible de libérer le bloc mémoire référencé par p en cas d’erreur puisque celui-ci serait devenu un pointeur nul. Il est donc impératif d’utiliser une seconde variable afin d’éviter des fuites de mémoire.

Les tableaux multidimensionnels

L’allocation de tableaux multidimensionnels est un petit peu plus complexe que celles des autres objets. Techniquement, il existe deux solutions : l’allocation d’un seul bloc de mémoire (comme pour les tableaux simples) et l’allocation de plusieurs tableaux eux-mêmes référencés par les éléments d’un autre tableau.

Allocation en un bloc

Comme pour un tableau simple, il vous est possible d’allouer un bloc de mémoire dont la taille correspond à la multiplication des longueurs de chaque dimension, elle-même multipliée par la taille d’un élément. Toutefois, cette solution vous contraint à effectuer une partie du calcul d’adresse vous-même puisque vous allouez en fait un seul tableau.

L’exemple ci-dessous illustre ce qui vient d’être dit en allouant un tableau à deux dimensions de trois fois trois int .

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = malloc(sizeof(int[3][3]));
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; i < 3; ++i)
        for (unsigned j = 0; j < 3; ++j)
        {
            p[(i * 3) + j] = (i * 3) + j;
            printf("p[%u][%u] = %d\n", i, j, p[(i * 3) + j]);
        }
    free(p);
    return 0;
}
Résultat
p[0][0] = 0
p[0][1] = 1
p[0][2] = 2
p[1][0] = 3
p[1][1] = 4
p[1][2] = 5
p[2][0] = 6
p[2][1] = 7
p[2][2] = 8

Comme vous le voyez, une partie du calcul d’adresse doit être effectuée en multipliant le premier indice par la longueur de la première dimension, ce qui permet d’arriver à la bonne « ligne ». Ensuite, il ne reste plus qu’à sélectionner le bon élément de la « colonne » à l’aide du second indice.

Bien qu’un petit peu plus complexe quant à l’accès aux éléments, cette solution à l’avantage de n’effectuer qu’une seule allocation de mémoire.

Dans ce cas ci, la taille du tableau étant connue à l’avance, nous aurions pu définir le pointeur p comme un pointeur sur un tableau de 3 int et ainsi nous affranchir du calcul d’adresse.

int (*p)[3] = malloc(sizeof(int[3][3]));

Allocation de plusieurs tableaux

La seconde solution consiste à allouer plusieurs tableaux plus un autre qui les référencera. Dans le cas d’un tableau à deux dimensions, cela signifie allouer un tableau de pointeurs dont chaque élément se verra affecter l’adresse d’un tableau également alloué dynamiquement. Cette technique nous permet d’accéder aux éléments des différents tableaux de la même manière que pour un tableau multidimensionnel puisque nous utilisons cette fois plusieurs tableaux.

L’exemple ci-dessous revient au même que le précédent, mais utilise le procédé qui vient d’être décrit. Notez que puisque nous réservons un tableau de pointeurs sur int , l’adresse de celui-ci doit être stockée dans un pointeur de pointeur sur int (rappelez-vous la règle générale lors de la présentation de la fonction malloc() ).

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int **p = malloc(sizeof(int[3]));
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; i < 3; ++i)
    {
        p[i] = malloc(sizeof(int[3]));
        if (p[i] == NULL)
        {
            printf("Échec de l'allocation\n");
            return EXIT_FAILURE;
        }
    }
    for (unsigned i = 0; i < 3; ++i)
        for (unsigned j = 0; j < 3; ++j)
        {
            p[i][j] = (i * 3) + j;
            printf("p[%u][%u] = %d\n", i, j, p[i][j]);
        }
    for (unsigned i = 0; i < 3; ++i)
        free(p[i]);
    free(p);
    return 0;
}

Si cette solution permet de faciliter l’accès aux différents éléments, elle présente toutefois l’inconvénient de réaliser plusieurs allocations et donc de nécessiter plusieurs appels à la fonction free() .

Les tableaux de longueur variable

À côté de l’allocation dynamique manuelle (matérialisée par des appels aux fonctions malloc() et consœurs) que nous venons de voir, il existe une autre solution pour allouer dynamiquement de la mémoire : les tableaux de longueur variable (ou variable length arrays en anglais, souvent abrégé en VLA).

Définition

En fait, jusqu’à présent, nous avons toujours défini des tableaux dont la taille était soit déterminée, soit déterminable lors de la compilation.

int t1[3]; /* Taille déterminée */
int t2[] = { 1, 2, 3 }; // Taille déterminable

Toutefois, la taille d’un tableau étant spécifiée par une expression entière, il est parfaitement possible d’employer une variable à la place d’une constante entière. Ainsi, le code suivant est parfaitement correct.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
    size_t n;
    printf("Entrez la taille du tableau : ");
    if (scanf("%zu", &n) != 1)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    int tab[n];
    for (size_t i = 0; i < n; ++i)
    {
        tab[i] = i * 10;
        printf("tab[%zu] = %d\n", i, tab[i]);
    }
    return 0;
}
Résultat
Entrez la taille du tableau : 10
tab[0] = 0
tab[1] = 10
tab[2] = 20
tab[3] = 30
tab[4] = 40
tab[5] = 50
tab[6] = 60
tab[7] = 70
tab[8] = 80
tab[9] = 90

Dans le cas où la taille du tableau ne peut pas être déterminée à la compilation, le compilateur va lui-même ajouter des instructions en vue d’allouer et de libérer de la mémoire dynamiquement.

Un tableau de longueur variable ne peut pas être initialisé lors de sa définition. Ceci tient au fait que le compilateur ne peut effectuer aucune vérification quant à la taille de la liste d’initialisation étant donné que la taille du tableau ne sera connue qu’à l’exécution du programme.

int tab[n] = { 1, 2, 3 }; // Incorrect

Une structure ne peut pas contenir de tableaux de longueur variable.

Utilisation

Un tableau de longueur variable s’utilise de la même manière qu’un tableau, avec toutefois quelques ajustements. En effet, s’agissant d’un tableau, s’il est employé dans une expression, il sera converti en un pointeur sur son premier élément. Toutefois, se pose alors la question suivante.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
void affiche_tableau(int (*tab)[/* Heu ... ? */], unsigned n, unsigned m)
{
    for (unsigned i = 0; i < n; ++i)
        for (unsigned j = 0; j < m; ++j)
            printf("tab[%u][%u] = %d\n", i, j, tab[i][j]);
}
int
main(void)
{
    size_t n, m;
    printf("Entrez la longueur et la largeur du tableau : ");
    if (scanf("%zu %zu", &n, &m) != 2)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    int tab[n][m];
    affiche_tableau(tab, n, m);
    return 0;
}

Comment peut-on préciser le type du paramètre tab étant donné que la taille du tableau ne sera connue qu’à l’exécution ? :euh:
Dans un tel cas, il est nécessaire d’inverser l’ordre des paramètres afin d’utiliser ceux-ci pour préciser au compilateur que le type ne sera déterminé qu’à l’exécution. Partant, la définition de la fonction affiche_tableau() devient celle-ci.

void affiche_tableau(size_t n, size_t m, int (*tab)[m]);
{
    for (unsigned i = 0; i < n; ++i)
        for (unsigned j = 0; j < m; ++j)
            printf("tab[%u][%u] = %d\n", i, j, tab[i][j]);
}

Ainsi, nous précisons au compilateur que le paramètre tab sera un pointeur sur un tableau de m int , m n’étant connu qu’à la compilation.

Application en dehors des tableaux de longueur variable

Notez que cette syntaxe n’est pas réservée aux tableaux de longueur variable. En effet, si elle est obligatoire dans le cas de leur utilisation, elle peut être employée dans d’autres situations. Si nous reprenons notre exemple d’allocation d’un tableau multidimensionnel en un bloc et le modifions de sorte que l’utilisateur puisse spécifier la taille de ses dimensions, nous pouvons écrire ceci et dès lors éviter le calcul d’adresse manuel.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    size_t n, m;
    printf("Entrez la longueur et la largeur du tableau : ");
    if (scanf("%zu %zu", &n, &m) != 2)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    int (*p)[m] = malloc(sizeof(int[n][m]));
    if (p == NULL)
    {
        printf("Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    for (size_t i = 0; i < n; ++i)
        for (size_t j = 0; j < m; ++j)
        {
            p[i][j] = (i * n) + j;
            printf("p[%zu][%zu] = %d\n", i, j, p[i][j]);
        }
    free(p);
    return 0;
}
Résultat
Entrez la longueur et la largeur du tableau : 3 3
p[0][0] = 0
p[0][1] = 1
p[0][2] = 2
p[1][0] = 3
p[1][1] = 4
p[1][2] = 5
p[2][0] = 6
p[2][1] = 7
p[2][2] = 8

Limitations

Pourquoi dans ce cas s’ennuyer avec l’allocation dynamique manuelle me direz-vous ? En effet, si le compilateur peut se charger de ce fardeau, autant le lui laisser, non ? Eh bien, parce que si cette méthode peut s’avérer salvatrice dans certains cas, elle souffre de limitations par rapport à l’allocation dynamique manuelle.

Durée de vie

La première limitation est que la classe de stockage des tableaux de longueur variable est automatique. Finalement, c’est assez logique, puisque le compilateur se charge de tout, il faut bien fixer le moment où la mémoire sera libérée. Du coup, comme pour n’importe quelle variable automatique, il est incorrect de retourner l’adresse d’un tableau de longueur variable puisque celui-ci n’existera plus une fois la fonction terminée.

int *ptr(size_t n)
{
    int tab[n];
    return tab; /* Incorrect */
}

Goto

La seconde limitation tient au fait qu’une allocation dynamique est réalisée lors de la définition du tableau de longueur variable. En effet, finalement, la définition d’un tableau de longueur variable peut être vue comme suit.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
    size_t n;
    printf("Entrez la taille du tableau : ");
    if (scanf("%zu", &n) != 1)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    int tab[n]; /* int *tab = malloc(sizeof(int) * n); */
    /* free(tab); */
    return 0;
}

Or, assez logiquement, si l’allocation est passée, typiquement avec une instruction de saut comme goto , nous risquons de rencontrer des problèmes… Dès lors, le code suivant est incorrect.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
    size_t n;
    printf("Entrez la taille du tableau : ");
    if (scanf("%zu", &n) != 1)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    goto boucle; /* Faux, car on saute l'allocation */
    if (n < 1000)
    {
        int tab[n]; /* int *tab = malloc(sizeof(int) * n); */
    boucle:
        for (size_t i = 0; i < n; ++i)
            tab[i] = i * 10;
        /* free(tab); */
    }
    return 0;
}

Dit plus formellement, il n’est pas permis de sauter après la définition d’un tableau de longueur variable si le saut est effectué en dehors de sa portée.

Taille des objets alloués

Une autre limitation vient du fait que la quantité de mémoire pouvant être allouée de cette manière est plus retreinte qu’avec malloc() et consœurs. Ceci tient à la manière dont la mémoire est allouée par le compilateur dans le cas des tableaux de longueur variable, qui diffère de celle employée par malloc() . Nous n’entrerons pas dans les détails ne s’agissant pas de l’objet de ce cours, mais globalement et de manière générale, évitez d’allouer un tableau de longueur variable si vous avez besoin de plus de 4 194 304 multiplets (soit le plus souvent environ 4 mégaoctets).

Gestion d’erreur

La dernière limitation tient à l’impossibilité de mettre en place une gestion d’erreur. En effet, il n’est pas possible de savoir si l’allocation a réussi ou échouer et votre programme est dès lors condamné à s’arrêter brutalement si cette dernière échoue.

Ce chapitre nous aura permis de découvrir la notion d’objet ainsi que le mécanisme de l’allocation dynamique. Dans le chapitre suivant, nous verrons comment manipuler les fichiers.

En résumé
  1. Un objet est une zone mémoire pouvant contenir des données composée d’une suite contiguë d’un ou plusieurs multiplets ;
  2. La représentation d’un type détermine de quelle manière les données sont réparties au sein d’un objet de ce type ;
  3. Il est possible de lire le contenu d’un objet multiplet par multiplet à l’aide d’un pointeur sur unsigned char ;
  4. La fonction malloc() permet d’allouer dynamiquement un objet d’une taille donnée ;
  5. La fonction calloc() fait de même en initialisant chaque multiplet à zéro ;
  6. La valeur zéro n’est pas forcément représentée en initialisant tous les multiplets d’un objet à zéro, cela dépend des types ;
  7. La fonction realloc() permet d’augmenter ou de diminuer la taille d’un bloc précédemment alloué avec malloc() ou calloc() ;
  8. Il est possible de préciser la taille d’un tableau à l’aide d’une variable auquel cas le compilateur se charge pour nous d’allouer et de libérer de la mémoire pour ce tableau ;
  9. L’utilisation de tels tableaux souffre de limitations par rapport à l’allocation dynamique manuelle.


Les fichiers (1) - Le langage C • Tutoriels • Zeste de Savoir

Les fichiers (1)

Les fichiers

En informatique, un fichier est un ensemble d’informations stockées sur un support, réuni sous un même nom et manipulé comme une unité.

Extension de fichier

Le contenu de chaque fichier est lui-même organisé suivant un format qui dépend des données qu’il contient. Il en existe une pléthore pour chaque type de données :

  • audio : Ogg, MP3, MP4, FLAC, Wave, etc. ;
  • vidéo : Ogg, WebM, MP4, AVI, etc. ;
  • documents : ODT, DOC et DOCX, XLS et XLSX, PDF, Postscript, etc.

Afin d’aider l’utilisateur, ces formats sont le plus souvent indiqués à l’aide d’une extension qui se traduit par un suffixe ajouté au nom de fichier. Toutefois, cette extension est purement indicative et facultative , elle n’influence en rien le contenu du fichier. Son seul objectif est d’aider à déterminer le type de contenu d’un fichier.

Système de fichiers

Afin de faciliter leur localisation et leur gestion, les fichiers sont classés et organisés sur leur support suivant un système de fichiers. C’est lui qui permet à l’utilisateur de répartir ses fichiers dans une arborescence de dossiers et de localiser ces derniers à partir d’un chemin d’accès.

Le chemin d’accès

Le chemin d’accès d’un fichier est une suite de caractères décrivant la position de celui-ci dans le système de fichiers. Un chemin d’accès se compose au minimum du nom du fichier visé et au maximum de la suite de tous les dossiers qu’il est nécessaire de traverser pour l’atteindre depuis le répertoire racine. Ces éventuels dossiers à traverser sont séparés par un caractère spécifique qui est / sous Unixoïde et \ sous Windows.

La racine d’un système de fichier est le point de départ de l’arborescence des fichiers et dossiers. Sous Unixoïdes, il s’agit du répertoire / tandis que sous Windows, chaque lecteur est une racine (comme C: par exemple). Si un chemin d’accès commence par la racine, alors celui-ci est dit absolu car il permet d’atteindre le fichier depuis n’importe quelle position dans l’arborescence. Si le chemin d’accès ne commence pas par la racine, il est dit relatif et ne permet de parvenir au fichier que depuis un point précis dans la hiérarchie de fichiers.

Ainsi, si nous souhaitons accéder à un fichier nommé « texte.txt » situé dans le dossier « documents » lui-même situé dans le dossier « utilisateur » qui est à la racine, alors le chemin d’accès absolu vers ce fichier serait /utilisateur/documents/texte.txt sous Unixoïde et C:\utilisateur\documents\texte.txt sous Windows (à supposer qu’il soit sur le lecteur C: ). Toutefois, si nous sommes déjà dans le dossier « utilisateur », alors nous pouvons y accéder à l’aide du chemin relatif documents/texte.txt ou documents\texte.txt . Néanmoins, ce chemin relatif n’est utilisable que si nous sommes dans le dossier « utilisateur ».

Métadonnées

Également, le système de fichier se charge le plus souvent de conserver un certain nombre de données concernant chaque fichier comme :

  • sa taille ;
  • ses droits d’accès (les utilisateurs autorisés à le manipuler) ;
  • la date de dernier accès ;
  • la date de dernière modification ;
  • etc.

Les flux : un peu de théorie

La bibliothèque standard vous fournit différentes fonctions pour manipuler les fichiers, toutes déclarées dans l’en-tête <stdio.h> . Toutefois, celles-ci manipulent non pas des fichiers, mais des flux de données en provenance ou à destination de fichiers. Ces flux peuvent être de deux types :

  • des flux de textes qui sont des suites de caractères terminées par un caractère de fin de ligne ( \n ) et formant ainsi des lignes ;
  • des flux binaires qui sont des suites de multiplets.

Pourquoi utiliser des flux ?

Pourquoi recourir à des flux plutôt que de manipuler directement des fichiers nous demanderez-vous ? Pour deux raisons : les disparités entre système d’exploitation quant à la représentation des lignes et la lourdeur des opérations de lecture et d’écriture.

Disparités entre systèmes d’exploitation

Les différents systèmes d’exploitation ne représentent pas les lignes de la même manière :

  • sous Mac OS (avant Mac OS X), la fin d’une ligne était indiquée par le caractère \r ;
  • sous Windows, la fin de ligne est indiquée par la suite de caractères \r\n ;
  • sous Unixoïdes (GNU/Linux, *BSD, Mac OS X, Solaris, etc.), la fin de ligne est indiquée par le caractère \n .

Aussi, si nous manipulions directement des fichiers, nous devrions prendre en compte ces disparités, ce qui rendrait notre code nettement plus pénible à rédiger. En passant par les fonctions de la bibliothèque standard, nous évitons ce casse-tête car celle-ci remplace le ou les caractères de fin de ligne par un \n (ce que nous avons pu expérimenter avec les fonctions printf() et scanf() ) et inversement.

Lourdeur des opérations de lecture et d’écriture

Lire depuis un fichier ou écrire dans un fichier signifie le plus souvent accéder au disque dur. Or, rappelez-vous, il s’agit de la mémoire la plus lente d’un ordinateur. Dès lors, si pour lire une ligne il était nécessaire de récupérer les caractères un à un depuis le disque dur, cela prendrait un temps fou.

Pour éviter ce problème, les flux de la bibliothèque standard recourent à un mécanisme appelé la temporisation ou mémorisation ( buffering en anglais). La bibliothèque standard fournit deux types de temporisation :

  • la temporisation par blocs ;
  • et la temporisation par lignes.

Avec la temporisation par blocs, les données sont récupérées depuis le fichier et écrites dans le fichier sous forme de blocs d’une taille déterminée. L’idée est la suivante : plutôt que de lire les caractères un à un, nous allons demander un bloc de données d’une taille déterminée que nous conserverons en mémoire vive pour les accès suivants. Cette technique est également utilisée lors des opérations d’écritures : les données sont stockées en mémoire jusqu’à ce qu’elles atteignent la taille d’un bloc. Ainsi, le nombre d’accès au disque dur sera limité à la taille totale du fichier à lire (ou des données à écrire) divisée par la taille d’un bloc.

La temporisation par lignes, comme son nom l’indique, se contente de mémoriser une ligne. Techniquement, celle-ci est utilisée lorsque le flux est associé à un périphérique interactif comme un terminal. En effet, si nous appliquions la temporisation par blocs pour les entrées de l’utilisateur, ce dernier devrait entrer du texte jusqu’à atteindre la taille d’un bloc. Pareillement, nous devrions attendre d’avoir écrit une quantité de données égale à la taille d’un bloc pour que du texte soit affiché à l’écran. Cela serait assez gênant et peu interactif… À la place, c’est la temporisation par lignes qui est utilisée : les données sont stockées jusqu’à ce qu’un caractère de fin de ligne soit rencontré ( \n , donc) ou jusqu’à ce qu’une taille maximale soit atteinte.

La bibliothèque standard vous permet également de ne pas temporiser les données en provenance ou à destination d’un flux.

stdin, stdout et stderr

Par défaut, trois flux de texte sont ouverts lors du démarrage d’un programme et sont déclarés dans l’en-tête <stdio.h> : stdin , stdout et stderr .

  • stdin correspond à l’entrée standard, c’est-à-dire le flux depuis lequel vous pouvez récupérer les informations fournies par l’utilisateur.
  • stdout correspond à la sortie standard, il s’agit du flux vous permettant de transmettre des informations à l’utilisateur qui seront le plus souvent affichées dans le terminal.
  • stderr correspond à la sortie d’erreur standard, c’est ce flux que vous devez privilégier lorsque vous souhaitez transmettre des messages d’erreurs ou des avertissements à l’attention de l’utilisateur (nous verrons comment l’utiliser un peu plus tard dans ce chapitre). Comme pour stdout , ces données sont le plus souvent affichées dans le terminal.

Les flux stdin et stdout sont temporisés par lignes (sauf s’ils sont associés à des fichiers au lieu de périphériques interactifs, auxquels cas ils seront temporisés par blocs) tandis que le flux stderr est au plus temporisé par lignes (ceci afin que les informations soient transmises le plus rapidement possible à l’utilisateur).

Ouverture et fermeture d'un flux

La fonction fopen

FILE *fopen(char *chemin, char *mode);

La fonction fopen() permet d’ouvrir un flux. Celle-ci attend deux arguments : un chemin d’accès vers un fichier qui sera associé au flux et un mode qui détermine le type de flux (texte ou binaire) et la nature des opérations qui seront réalisées sur le fichier via le flux (lecture, écriture ou les deux). Elle retourne un pointeur vers un flux en cas de succès et un pointeur nul en cas d’échec.

Le mode

Le mode est une chaîne de caractères composée d’une ou plusieurs lettres qui décrit le type du flux et la nature des opérations qu’il doit réaliser.

Cette chaîne commence obligatoirement par la seconde de ces informations. Il existe six possibilités reprises dans le tableau ci-dessous.

Mode | Type(s) d’opération(s) | Effets
r | Lecture | Néant
r+ | Lecture et écriture | Néant
w | Écriture | Si le fichier n’existe pas, il est créé. Si le fichier existe, son contenu est effacé.
w+ | Lecture et écriture | Idem
a | Écriture | Si le fichier n’existe pas, il est créé. Place les données à la fin du fichier
a+ | Lecture et écriture | Idem

Par défaut, les flux sont des flux de textes. Pour obtenir un flux binaire, il suffit d’ajouter la lettre b à la fin de la chaîne décrivant le mode.

Exemple

Le code ci-dessous tente d’ouvrir un fichier nommé « texte.txt » en lecture seule dans le dossier courant. Notez que dans le cas où il n’existe pas, la fonction fopen() retournera un pointeur nul (seul le mode r permet de produire ce comportement).

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp = fopen("texte.txt", "r");
    if (fp == NULL)
    {
        printf("Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    printf("Le fichier texte.txt existe\n");
    return 0;
}

La fonction fclose

int fclose(FILE *flux);

La fonction fclose() termine l’association entre un flux et un fichier. S’il reste des données temporisées, celles-ci sont écrites. La fonction retourne zéro en cas de succès et EOF en cas d’erreur.

EOF est une constante définie dans l’en-tête <stdio.h> et est utilisée par les fonctions déclarées dans ce dernier pour indiquer soit l’arrivée à la fin d’un fichier (nous allons y venir) soit la survenance d’une erreur. La valeur de cette constante est toujours un entier négatif .

Nous pouvons désormais compléter l’exemple précédent comme suit.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp = fopen("texte.txt", "r");
    if (fp == NULL)
    {
        printf("Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    printf("Le fichier texte.txt existe\n");
    if (fclose(fp) == EOF)
    {
        printf("Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    return 0;
}

Veillez qu’à chaque appel à la fonction fopen() corresponde un appel à la fonction fclose() .

Écriture vers un flux de texte

Écrire un caractère

int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);
int putchar(int ch);

Les fonctions putc() et fputc() écrivent un caractère dans un flux. Il s’agit de l’opération d’écriture la plus basique sur laquelle reposent toutes les autres fonctions d’écriture. Ces deux fonctions retournent soit le caractère écrit, soit EOF si une erreur est rencontrée. La fonction putchar() , quant à elle, est identique aux fonctions putc() et fputc() si ce n’est qu’elle écrit dans le flux stdout .

Techniquement, putc() et fputc() sont identiques, si ce n’est que putc() est en fait le plus souvent une macrofonction. Étant donné que nous n’avons pas encore vu de quoi il s’agit, préférez utiliser la fonction fputc() pour l’instant.

L’exemple ci-dessous écrit le caractère « C » dans le fichier « texte.txt ». Étant donné que nous utilisons le mode w , le fichier est soit créé s’il n’existe pas, soit vidé de son contenu s’il existe (revoyez le tableau des modes à la section précédente si vous êtes perdus).

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp = fopen("texte.txt", "w");
    if (fp == NULL)
    {
        printf("Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fputc('C', fp) == EOF)
    {
        printf("Erreur lors de l'écriture d'un caractère\n");
        return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        printf("Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    return 0;
}

Écrire une ligne

int fputs(char *ligne, FILE *flux);
int puts(char *ligne);

La fonction fputs() écrit une ligne dans le flux flux . La fonction retourne un nombre positif ou nul en cas de succès et EOF en cas d’erreurs. La fonction puts() est identique si ce n’est qu’elle ajoute automatiquement un caractère de fin de ligne et qu’elle écrit sur le flux stdout .

Maintenant que nous savons comment écrire une ligne dans un flux précis, nous allons pouvoir diriger nos messages d’erreurs vers le flux stderr afin que ceux-ci soient affichés le plus rapidement possible.

L’exemple suivant écrit le mot « Bonjour » suivi d’un caractère de fin de ligne au sein du fichier « texte.txt ».

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp = fopen("texte.txt", "w");
    if (fp == NULL)
    {
        fputs("Le fichier texte.txt n'a pas pu être ouvert\n", stderr);
        return EXIT_FAILURE;
    }
    if (fputs("Bonjour\n", fp) == EOF)
    {
        fputs("Erreur lors de l'écriture d'une ligne\n", stderr);
        return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fputs("Erreur lors de la fermeture du flux\n", stderr);
        return EXIT_FAILURE;
    }
    return 0;
}

La norme 1 vous garantit qu’une ligne peut contenir jusqu’à 254 caractères (caractère de fin de ligne inclus). Aussi, veillez à ne pas écrire de ligne d’une taille supérieure à cette limite.

La fonction fprintf

int fprintf(FILE *flux, char *format, ...);

La fonction fprintf() est la même que la fonction printf() si ce n’est qu’il est possible de lui spécifier sur quel flux écrire (au lieu de stdout pour printf() ). Elle retourne le nombre de caractères écrits ou une valeur négative en cas d’échec.

  1. ISO/IEC 9899:201x, doc. N1570, § 7.21.2, Streams, al. 9.

    ↩

Lecture depuis un flux de texte

Récupérer un caractère

int getc(FILE *flux);
int fgetc(FILE *flux);
int getchar(void);

Les fonctions getc() et fgetc() sont les exacts miroirs des fonctions putc() et fputc() : elles récupèrent un caractère depuis le flux fourni en argument. Il s’agit de l’opération de lecture la plus basique sur laquelle reposent toutes les autres fonctions de lecture. Ces deux fonctions retournent soit le caractère lu, soit EOF si la fin de fichier est rencontrée ou si une erreur est rencontrée. La fonction getchar() , quant à elle, est identique à ces deux fonctions si ce n’est qu’elle récupère un caractère depuis le flux stdin .

Comme putc() , la fonction getc() est le plus souvent une macrofonction. Utilisez donc plutôt la fonction fgetc() pour le moment.

L’exemple ci-dessous lit un caractère provenant du fichier texte.txt .

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp = fopen("texte.txt", "r");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    int ch = fgetc(fp);
    if (ch != EOF)
        printf("%c\n", ch);
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    return 0;
}

Notez que nous utilisons une affectation comme premier opérande de l’opérateur != . Une affectation étant une expression en C, ce genre d’écriture est tout à fait valide. Vous en rencontrerez fréquemment comme expression de contrôle de boucles.

Récupérer une ligne

char *fgets(char *tampon, int taille, FILE *flux);

La fonction fgets() lit une ligne depuis le flux flux et la stocke dans le tableau tampon . Cette dernière lit au plus un nombre de caractères égal à taille diminué de un afin de laisser la place pour le caractère nul, qui est automatiquement ajouté. Dans le cas où elle rencontre un caractère de fin de ligne : celui-ci est conservé au sein du tableau , un caractère nul est ajouté et la lecture s’arrête.

La fonction retourne l’adresse du tableau tampon en cas de succès et un pointeur nul si la fin du fichier est atteinte ou si une erreur est survenue .

L’exemple ci-dessous réalise donc la même opération que le code précédent, mais en utilisant la fonction fgets() .

Étant donné que la norme nous garantit qu’une ligne peut contenir jusqu’à 254 caractères (caractère de fin de ligne inclus), nous utilisons un tableau de 255 caractères pour les contenir (puisqu’il est nécessaire de prévoir un espace pour le caractère nul).

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char buf[255];
    FILE *fp = fopen("texte.txt", "r");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fgets(buf, sizeof buf, fp) != NULL)
        printf("%s\n", buf);
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    return 0;
}

Toutefois, il y a un petit problème : la fonction fgets() conserve le caractère de fin de ligne qu’elle rencontre. Dès lors, nous affichons deux retours à la ligne : celui contenu dans la chaîne buf et celui affiché par printf() . Aussi, il serait préférable d’en supprimer un, de préférence celui de la chaîne de caractères. Pour ce faire, nous pouvons faire appel à une petite fonction (que nous appellerons chomp() en référence à la fonction éponyme du langage Perl) qui se chargera de remplacer le caractère de fin de ligne par un caractère nul.

void chomp(char *s)
{
    while (*s != '\n' && *s != '\0')
        ++s;
    *s = '\0';
}

La fonction fscanf

int fscanf(FILE *flux, char *format, ...);

La fonction fscanf() est identique à la fonction scanf() si ce n’est qu’elle récupère les données depuis le flux fourni en argument (au lieu de stdin pour scanf() ).

Le flux stdin étant le plus souvent mémorisé par lignes, ceci vous explique pourquoi nous lisions les caractères restant après un appel à scanf() jusqu’à rencontrer un caractère de fin de ligne : pour vider le tampon du flux stdin .

La fonction fscanf() retourne le nombre de conversions réussies (voire zéro, si aucune n’est demandée ou n’a pu être réalisée) ou EOF si une erreur survient avant qu’une conversion n’ait eu lieu.

Écriture vers un flux binaire

Écrire un multiplet

int putc(int ch, FILE *flux);
int fputc(int ch, FILE *flux);

Comme pour les flux de texte, il vous est possible de recourir aux fonctions putc() et fputc() . Dans le cas d’un flux binaire, ces fonctions écrivent un multiplet (sous la forme d’un int converti en unsigned char ) dans le flux spécifié.

Écrire une suite de multiplets

size_t fwrite(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fwrite() écrit le tableau référencé par ptr composé de nombre éléments de taille multiplets dans le flux flux . Elle retourne une valeur égale à nombre en cas de succès et une valeur inférieure en cas d’échec.

L’exemple suivant écrit le contenu du tableau tab dans le fichier binaire.bin . Dans le cas où un int fait 4 octets, 20 octets seront donc écrits.

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int tab[5] = { 1, 2, 3, 4, 5 };
    const size_t n = sizeof tab / sizeof tab[0];
    FILE *fp = fopen("binaire.bin", "wb");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier binaire.bin n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fwrite(&tab, sizeof tab[0], n, fp) != n)
    {
            fprintf(stderr, "Erreur lors de l'écriture du tableau\n");
            return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    return 0;
}

Lecture depuis un flux binaire

Lire un multiplet

int getc(FILE *flux);
int fgetc(FILE *flux);

Lors de la lecture depuis un flux binaire, les fonctions fgetc() et getc() permettent de récupérer un multiplet (sous la forme d’un unsigned char converti en int ) depuis un flux.

Lire une suite de multiplets

size_t fread(void *ptr, size_t taille, size_t nombre, FILE *flux);

La fonction fread() est l’inverse de la fonction fwrite() : elle lit nombre éléments de taille multiplets depuis le flux flux et les stocke dans l’objet référencé par ptr . Elle retourne une valeur égale à nombre en cas de succès ou une valeur inférieure en cas d’échec.

Dans le cas où nous disposons du fichier binaire.bin produit par l’exemple de la section précédente, nous pouvons reconstituer le tableau tab .

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int tab[5] = { 1, 2, 3, 4, 5 };
    const size_t n = sizeof tab / sizeof tab[0];
    FILE *fp = fopen("binaire.bin", "rb");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier binaire.bin n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fread(&tab, sizeof tab[0], n, fp) != n)
    {
            fprintf(stderr, "Erreur lors de la lecture du tableau\n");
            return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; i < n; ++i)
        printf("tab[%u] = %d\n", i, tab[i]);
    return 0;
}
Résultat
tab[0] = 1
tab[1] = 2
tab[2] = 3
tab[3] = 4
tab[4] = 5

Dans le chapitre suivant, nous continuerons notre découverte des fichiers en attaquant quelques notions plus avancées : la gestion d’erreur et de fin de fichier, le déplacement au sein d’un flux, la temporisation et les subtilités liées aux flux ouverts en lecture et écriture.

En résumé
  1. L’extension d’un fichier fait partie de son nom et est purement indicative ;
  2. Les fichiers sont manipulés à l’aide de flux afin de s’abstraire des disparités entre systèmes d’exploitation et de recourir à la temporisation ;
  3. Trois flux de texte sont disponibles par défaut : stdin , stdout et stderr ;
  4. La fonction fopen() permet d’associer un fichier à un flux, la fonction fclose() met fin à cette association ;
  5. Les fonctions fputc() , fputs() et fprintf() écrivent des données dans un flux de texte ;
  6. Les fonctions fgetc() , fgets() et fscanf() lisent des données depuis un flux de texte ;
  7. Les fonctions fputc() et fwrite() écrivent un ou plusieurs multiplets dans un flux binaire ;
  8. Les fonctions fgetc() et fread() lisent un ou plusieurs multiplets depuis un flux binaire.


Les fichiers (2) - Le langage C • Tutoriels • Zeste de Savoir

Les fichiers (2)

Détection d'erreurs et fin de fichier

Lors de la présentation des fonctions fgetc() , getc() et fgets() vous avez peut-être remarqué que ces fonctions utilisent un même retour pour indiquer soit la rencontre de la fin du fichier, soit la survenance d’une erreur. Du coup, comment faire pour distinguer l’un et l’autre cas ?

Il existe deux fonctions pour clarifier une telle situation : feof() et ferror() .

La fonction feof

int feof(FILE *flux);

La fonction feof() retourne une valeur non nulle dans le cas où la fin du fichier associé au flux spécifié est atteinte.

La fonction ferror

int ferror(FILE *flux);

La fonction ferror() retourne une valeur non nulle dans le cas où une erreur s’est produite lors d’une opération sur le flux visé.

Exemple

L’exemple ci-dessous utilise ces deux fonctions pour déterminer le type de problème rencontré par la fonction fgets() .

#include <stdio.h>
int main(void)
{
    char buf[255];
    FILE *fp =  fopen("texte.txt", "r");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fgets(buf, sizeof buf, fp) != NULL)
        printf("%s\n", buf);
    else if (ferror(fp))
    {
        fprintf(stderr, "Erreur lors de la lecture\n");
        return EXIT_FAILURE;
    }
    else
    {
        fprintf(stderr, "Fin de fichier rencontrée\n");
        return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    return 0;
}

Position au sein d'un flux

De manière imagée, un fichier peut être vu comme une longue bande magnétique depuis laquelle des données sont lues ou sur laquelle des informations sont écrites. Ainsi, au fur et à mesure que les données sont lues ou écrites, nous avançons le long de cette bande.

Toutefois, il est parfois nécessaire de se rendre directement à un point précis de cette bande (par exemple à la fin) afin d’éviter des lectures inutiles pour parvenir à un endroit souhaité. À cet effet, la bibliothèque standard fournit plusieurs fonctions.

La fonction ftell

long ftell(FILE *flux);

La fonction ftell() retourne la position actuelle au sein du fichier sous forme d’un long . Elle retourne un nombre négatif en cas d’erreur. Dans le cas des flux binaires , cette valeur correspond au nombre de multiplets séparant la position actuelle du début du fichier.

Lors de l’ouverture d’un flux, la position courante correspond au début du fichier, sauf pour les modes a et a+ pour lesquels la position initiale est soit le début, soit la fin du fichier.

La fonction fseek

int fseek(FILE *flux, long distance, int repere);

La fonction fseek() permet d’effectuer un déplacement d’une distance fournie en argument depuis un repère donné. Elle retourne zéro en cas de succès et une autre valeur en cas d’échec. Il existe trois repères possibles :

  • SEEK_SET qui correspond au début du fichier ;
  • SEEK_CUR qui correspond à la position courante ;
  • SEEK_END qui correspond à la fin du fichier.

Cette fonction s’utilise différemment suivant qu’elle opère sur un flux de texte ou sur un flux binaire.

Les flux de texte

Dans le cas d’un flux de texte, il y a deux possibilités :

  • la distance fournie est nulle ;
  • la distance est une valeur fournie par un précédent appel à la fonction ftell() et le repère est SEEK_SET .

Les flux binaires

Dans le cas d’un flux binaire, seuls les repères SEEK_SET et SEEK_CUR peuvent être utilisés. La distance correspond à un nombre de multiplets à passer depuis le repère fourni.

Dans le cas des modes a et a+ , un déplacement a lieu à la fin du fichier avant chaque opération d’écriture et ce, qu’il y ait eu déplacement auparavant ou non .

La fonction fgetpos

int fgetpos(FILE *flux, fpos_t *position);

La fonction fgetpos() écrit la position courante dans un objet de type fpos_t référencé par position . Elle retourne zéro en cas de succès et un nombre non nul dans le cas contraire.

La fonction fsetpos

int fsetpos(FILE *flux, fpos_t *position);

La fonction fsetpos() effectue un déplacement à la position précédemment écrite dans un objet de type fpos_t référencé par position . En cas de succès, elle retourne zéro, sinon un nombre entier non nul.

La fonction rewind

void rewind(FILE *flux);

La fonction rewind() vous ramène au début du fichier (autrement dit, elle rembobine la bande).

Si vous utilisez un flux ouvert en lecture et écriture vous devez appeler une fonction de déplacement entre deux opérations de natures différentes ou utiliser la fonction fflush() (présentée dans l’extrait suivant) entre une opération d’écriture et de lecture. Si vous ne souhaitez pas vous déplacer, vous pouvez utilisez l’appel fseek(flux, 0, SEEK_CUR) afin de respecter cette condition sans réellement effectuer un déplacement.

La temporisation

Dans le chapitre précédent, nous vous avons précisé que les flux étaient le plus souvent temporisés afin d’optimiser les opérations de lecture et d’écriture sous-jacentes. Dans cette section, nous allons nous pencher un peu plus sur cette notion.

Introduction

Nous vous avons dit auparavant que deux types de temporisations existaient : la temporisation par lignes et celle par blocs. Une des conséquences logiques de cette temporisation est que les fonctions de lecture/écriture récupèrent les données et les inscrivent dans ces tampons. Ceci peut paraître évident, mais peut avoir des conséquences parfois surprenantes si ce fait est oublié ou inconnu.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char nom[64];
    char prenom[64];
    printf("Quel est votre nom ? ");
    if (scanf("%63s", nom) != 1)
    {
        fprintf(stderr, "Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    printf("Quel est votre prénom ? ");
    if (scanf("%63s", prenom) != 1)
    {
        fprintf(stderr, "Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    printf("Votre nom est %s\n", nom);
    printf("Votre prénom est %s\n", prenom);
    return 0;
}
Résultat
Quel est votre nom ? Charles Henri
Quel est votre prénom ? Votre nom est Charles
Votre prénom est Henri

Comme vous le voyez, le programme ci-dessus réalise deux saisies, mais si l’utilisateur entre par exemple « Charles Henri », il n’aura l’occasion d’entrer des données qu’une seule fois. Ceci est dû au fait que l’indicateur s récupère une suite de caractères exempte d’espaces (ce qui fait qu’il s’arrête à « Charles ») et que la suite « Henri » demeure dans le tampon du flux stdin . Ainsi, lors de la deuxième saisie, il n’est pas nécessaire de récupérer de nouvelles données depuis le terminal puisqu’il y en a déjà en attente dans le tampon, d’où le résultat obtenu.

Le même problème peut se poser si par exemple les données fournies ont une taille supérieure par rapport à l’objet qui doit les accueillir.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char chaine1[16];
    char chaine2[16];
    printf("Un morceau : ");
    if (fgets(chaine1, sizeof chaine1, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    printf("Un autre morceau : ");
    if (fgets(chaine2, sizeof chaine2, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    printf("%s ; %s\n", chaine1, chaine2);
    return 0;
}
Résultat
Un morceau : Une chaîne de caractères, vraiment, mais alors vraiment trop longue
Un autre morceau : Une chaîne de  ; caractères, vr

Ici, la chaîne entrée est trop importante pour être contenue dans chaine1 , les données non lues sont alors conservées dans le tampon du flux stdin et lues lors de la seconde saisie (qui ne lit par l’entièreté non plus).

Interargir avec la temporisation

Vider un tampon

Si la temporisation nous évite des coûts en terme d’opérations de lecture/écriture, il nous est parfois nécessaire de passer outre cette mécanique pour vider manuellement le tampon d’un flux.

Opération de lecture

Si le tampon contient des données qui proviennent d’une opération de lecture, celles-ci peuvent être abandonnées soit en appelant une fonction de positionnement soit en lisant les données, tout simplement. Il y a toutefois un bémol avec la première solution : les fonctions de positionnement ne fonctionnent pas dans le cas où le flux ciblé est lié à un périphérique « interactif », c’est-à-dire le plus souvent un terminal.

Autrement dit, pour que l’exemple précédent recoure bien à deux saisies, il nous est nécessaire de vérifier que la fonction fgets() a bien lu un caractère \n (qui signifie que la fin de ligne est atteinte et donc celle du tampon s’il s’agit du flux stdin ). Si ce n’est pas le cas, alors il nous faut lire les caractères restants jusqu’au \n final (ou la fin du fichier).

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int vider_tampon(FILE *fp)
{
    int c;
    do
        c = fgetc(fp);
    while (c != '\n' && c != EOF);
    return ferror(fp) ? 0 : 1;
}
int main(void)
{
    char chaine1[16];
    char chaine2[16];
    printf("Un morceau : ");
    if (fgets(chaine1, sizeof chaine1, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    if (strchr(chaine1, '\n') == NULL)
        if (!vider_tampon(stdin))
        {
            fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
            return EXIT_FAILURE;
        }
    printf("Un autre morceau : ");
    if (fgets(chaine2, sizeof chaine2, stdin) == NULL)
    {
        printf("Erreur lors de la saisie\n");
        return EXIT_FAILURE;
    }
    if (strchr(chaine2, '\n') == NULL)
        if (!vider_tampon(stdin))
        {
            fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
            return EXIT_FAILURE;
        }
    printf("%s ; %s\n", chaine1, chaine2);
    return 0;
}
Résultat
Un morceau : Une chaîne de caractères vraiment, mais alors vraiment longue
Un autre morceau : Une autre chaîne de caractères
Une chaîne de  ; Une autre chaî
Opération d’écriture

Si, en revanche, le tampon comprend des données en attente d’écriture, il est possible de forcer celle-ci soit à l’aide d’une fonction de positionnement, soit à l’aide de la fonction fflush() .

int fflush(FILE *flux);

Celle-ci vide le tampon du flux spécifié et retourne zéro en cas de succès ou EOF en cas d’erreur.

Notez bien que cette fonction ne peut être employée que pour vider un tampon comprenant des données en attente d' écriture .

Modifier un tampon

Techniquement, il ne vous est pas possible de modifier directement le contenu d’un tampon, ceci est réalisé par les fonctions de la bibliothèque standard au gré de vos opérations de lecture et/ou d’écriture. Il y a toutefois une exception à cette règle : la fonction ungetc() .

int ungetc(int ch, FILE *flux);

Cette fonction est un peu particulière : elle place un caractère ou un multiplet ch dans le tampon du flux flux . Celui-ci pourra être lu lors d’un appel ultérieur à une fonction de lecture . Elle retourne le caractère ou le multiplet ajouté en cas de succès et EOF en cas d’échec.

Cette fonction est très utile dans le cas où les actions d’un programme dépendent du contenu d’un flux. Imaginez par exemple que votre programme doit déterminer si l’entrée standard contient une suite de caractères ou un nombre et doit ensuite afficher celui-ci. Vous pourriez utiliser getchar() pour récupérer le premier caractère et déterminer s’il s’agit d’un chiffre. Toutefois, le premier caractère du flux est alors lu et cela complique votre tâche pour la suite… La fonction ungetc() vous permet de résoudre ce problème en replaçant ce caractère dans le tampon du flux stdin .

#include <stdio.h>
#include <stdlib.h>
static void afficher_nombre(void);
static void afficher_chaine(void);
static void afficher_nombre(void)
{
    double f;
    if (scanf("%lf", &f) == 1)
        printf("Vous avez entré le nombre : %f\n", f);
    else
        fprintf(stderr, "Erreur lors de la saisie\n");
}
static void afficher_chaine(void)
{
    char chaine[255];
    if (scanf("%254s", chaine) == 1)
        printf("Vous avez entré la chaine : %s\n", chaine);
    else
        fprintf(stderr, "Erreur lors de la saisie\n");
}
int main(void)
{
    int ch = getchar();
    if (ungetc(ch, stdin) == EOF)
    {
        fprintf(stderr, "Impossible de replacer un caractère\n");
        return EXIT_FAILURE;
    }
    switch (ch)
    {
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
        afficher_nombre();
        break;
    default:
        afficher_chaine();
        break;
    }
    return 0;
}
Résultat
Test
Vous avez entré la chaine : Test
52.5
Vous avez entré le nombre : 52.500000

La fonction ungetc() ne vous permet de replacer qu’ un seul caractère ou multiplet avant une opération de lecture .

Flux ouverts en lecture et écriture

Pour clore ce chapitre, un petit mot sur le cas des flux ouverts en lecture et en écriture (soit à l’aide des modes r+ , w+ , a+ et leurs équivalents binaires).

Si vous utilisez un tel flux, vous devez appeler une fonction de déplacement entre deux opérations de natures différentes ou utiliser la fonction fflush() entre une opération d’écriture et de lecture. Si vous ne souhaitez pas vous déplacer, vous pouvez utiliser l’appel fseek(flux, 0L, SEEK_CUR) afin de respecter cette condition sans réellement effectuer un déplacement.

Vous l’aurez sans doute compris : cette règle impose en fait de vider le tampon du flux entre deux opérations de natures différentes.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char chaine[255];
    FILE *fp = fopen("texte.txt", "w+");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert.\n");
        return EXIT_FAILURE;
    }
    if (fputs("Une phrase.\n", fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de l'écriture d'une ligne.\n");
        return EXIT_FAILURE;
    }
    /* Retour au début : la condition est donc remplie. */
    if (fseek(fp, 0L, SEEK_SET) != 0)
    {
        fprintf(stderr, "Impossible de revenir au début du fichier.\n");
        return EXIT_FAILURE;
    }
    if (fgets(chaine, sizeof chaine, fp) == NULL)
    {
        fprintf(stderr, "Erreur lors de la lecture.\n");
        return EXIT_FAILURE;
    }
    printf("%s\n", chaine);
    if (fclose(fp) == EOF)
    {
        fputs("Erreur lors de la fermeture du flux\n", stderr);
        return EXIT_FAILURE;
    }
    return 0;
}
Résultat
Une phrase.

Dans le chapitre suivant, nous ferons une petite pause dans notre apprentissage afin de réaliser un jeu de Puissance 4 .

En résumé
  1. La fonction feof() permet de savoir si la fin du fichier associé à un flux a été atteinte ;
  2. La fonction ferror() permet de savoir si une erreur est survenue lors d’une opération sur un flux ;
  3. La fonction ftell() retourne la position actuelle au sein d’un flux ;
  4. La fonction fseek() effectue un déplacement au sein d’un fichier depuis un point donné ;
  5. La fonction rewind() réalise un déplacement jusqu’au début d’un fichier ;
  6. Pour vider un tampon contenant des données en attente de lecture, il est nécessaire de lire ces dernières ;
  7. Afin de vider un tampon contenant des données en attente d’écriture, il est possible d’employer soit une fonction de positionnement, soit la fonction fflush() ;
  8. La fonction ungetc() permet de placer un caractère ou un multiplet dans un tampon en vue d’une lecture ;
  9. Lorsqu’un flux est ouvert en lecture et écriture, il est nécessaire d’appeler une fonction de déplacement entre deux opérations de natures différentes (ou d’appeler la fonction fflush() entre une opération d’écriture et de lecture).


Le préprocesseur - Le langage C • Tutoriels • Zeste de Savoir

Le préprocesseur

Les inclusions

Nous avons vu dès le début du cours comment inclure des fichiers d’en-tête avec la directive #include , sans toutefois véritablement expliquer son rôle. Son but est très simple : inclure le contenu d’un fichier dans un autre. Ainsi, si nous nous retrouvons par exemple avec deux fichiers comme ceux-ci avant la compilation.

fichier.h
#ifndef FICHIER_H
#define FICHIER_H
extern int glob_var;
extern void f1(int);
extern long f2(double, char);
#endif
fichier.c
#include "fichier.h"
void f1(int arg)
{
    /* du code */
}
long f2(double arg, char c)
{
    /* du code */
}

Après le passage du préprocesseur et avant la compilation à proprement parler, le code obtenu sera le suivant :

fichier.c
extern int glob_var;
extern void f1(int arg);
extern long f2(double arg, char c);
void f1(int arg)
{
    /* du code */
}
long f2(double arg, char c)
{
    /* du code */
}

Nous pouvons voir que les déclarations contenues dans le fichier « fichier.h » ont été incluses dans le fichier « fichier.c » et que toutes les directives du préprocesseur (les lignes commençant par le symbole # ) ont disparu.

Vous pouvez utiliser l’option -E lors de la compilation pour requérir uniquement l’utilisation du préprocesseur. Ainsi, vous pouvez voir ce que donne votre code après son passage.

$ zcc -E fichier.c

Les macroconstantes

Comme nous l’avons dit dans l’introduction, le préprocesseur permet la définition de macros, c’est-à-dire de substituts à des morceaux de code. Une macro est constituée des éléments suivants.

  • La directive #define .
  • Le nom de la macro qui, par convention, est souvent écrit en majuscules. Notez que vous pouvez choisir le nom que vous voulez, à condition de respecter les mêmes règles que pour les noms de variable ou de fonction.
  • Une liste optionnelle de paramètres.
  • La définition, c’est-à-dire le code par lequel la macro sera remplacée.

Dans le cas particulier où la macro n’a pas de paramètre, on parle de macroconstante (ou constante de préprocesseur). Leur substitution donne toujours le même résultat (d’où l’adjectif « constante »).

Substitutions de constantes

Par exemple, pour définir une macroconstante TAILLE avec pour valeur 100 , nous utiliserons le code suivant.

#define TAILLE 100

L’exemple qui suit utilise cette macroconstante afin de ne pas multiplier l’usage de constantes entières. À chaque fois que la macroconstante TAILLE est utilisée, le préprocesseur remplacera celle-ci par sa définition, à savoir 100 .

#include <stdio.h>
#define TAILLE 100
int main(void)
{
    int variable = 5;
    /* On multiplie par TAILLE */
    variable *= TAILLE;
    printf("Variable vaut : %d\n", variable);
    /* On additionne TAILLE */
    variable += TAILLE;
    printf("Variable vaut : %d\n", variable);
    return 0;
}

Ce code sera remplacé, après le passage du préprocesseur, par celui-ci.

int main(void)
{
    int variable = 5;
    variable *= 100;
    printf("Variable vaut : %d\n", variable);
    variable += 100;
    printf("Variable vaut : %d\n", variable);
    return 0;
}

Nous n’avons pas inclus le contenu de <stdio.h> car celui-ci est trop long et trop compliqué pour le moment. Néanmoins, l’exemple permet d’illustrer le principe des macros et surtout de leur avantage : il suffit de changer la définition de la macroconstante TAILLE pour que le reste du code s’adapte.

D’accord, mais je peux aussi très bien utiliser une variable constante, non ?

Dans certains cas (comme dans l’exemple ci-dessus), utiliser une variable constante donnera le même résultat. Toutefois, une variable nécessitera le plus souvent de réserver de la mémoire et, si celle-ci doit être partagée par différents fichiers, de jouer avec les déclarations et les définitions.

De plus, les macroconstantes peuvent être employées pour définir la longueur d’un tableau, alors que ceci est impossible avec une variable, même constante.

Notez qu’il vous est possible de définir une macroconstante lors de la compilation à l’aide de l’option -D .

zcc -DTAILLE=100

Ceci revient à définir une macroconstante TAILLE au début de chaque fichier qui sera substituée par 100 .

Substitutions d’instructions

Si les macroconstantes sont souvent utilisées en vue de substituer des constantes, il est toutefois aussi possible que leur définition comprenne des suites d’instructions. L’exemple ci-dessous définit une macroconstante BONJOUR qui sera remplacée par puts("Bonjour !") .

#include <stdio.h>
#define BONJOUR  puts("Bonjour !")
int main(void)
{
    BONJOUR;
    return 0;
}

Notez que nous n’avons pas placé de point-virgule dans la définition de la macroconstante, tout d’abord afin de pouvoir en placer un lors de son utilisation, ce qui est plus naturel et, d’autre part, pour éviter des doublons qui peuvent s’avérer fâcheux.

En effet, si nous ajoutons un point virgule à la fin de la définition, il ne faut pas en rajouter un lors de l’appel, sous peine de risquer des erreurs de syntaxe.

#include <stdio.h>
#define BONJOUR puts("Bonjour !");
#define AUREVOIR puts("Au revoir !");
int main(void)
{
    if (1)
        BONJOUR; /* erreur ! */
    else
        AUREVOIR;
    return 0;
}

Le code donnant en réalité ceci.

int main(void)
{
    if (1)
        puts("Bonjour !");;
    else
        puts("Au revoir !");;
    return 0;
}

Ce qui est incorrect. Pour faire simple : le corps d’une condition ne peut comprendre qu’une instruction, or puts("Bonjour !"); en est une, le point-virgule seul ( ; ) en est une autre. Dès lors, il y a une instruction entre le if et le else , ce qui n’est pas permis. L’exemple ci-dessous pose le même problème en étant un peu plus limpide.

#include <stdio.h>
int main(void)
{
    if (1)
        puts("Bonjour !");
    puts("Je suis une instruction mal placee !");
    else
        puts("Au revoir !");
    return 0;
}

Macros dans d’autres macros

La définition d’une macro peut parfaitement comprendre une ou plusieurs autres macros (qui, à leurs tours, peuvent également comprendre des macros dans leur définition et ainsi de suite). L’exemple suivant illustre cela en définissant une macroconstante NB_PIXELS correspondant à la multiplication entre les deux macroconstantes LONGUEUR et HAUTEUR .

#define LONGUEUR 1024
#define HAUTEUR 768
#define NB_PIXELS (LONGUEUR * LARGEUR)

La règle suivante permet d’éviter de répéter les opérations de remplacements « à l’infini » :

Une macro n’est pas substituée si elle est utilisée dans sa propre définition.

Ainsi, dans le code ci-dessous, LONGUEUR donnera LONGUEUR * 2 / 2 et HAUTEUR sera substituée par HAUTEUR / 2 * 2 . En effet, lors de la substitution de la macroconstante LONGUEUR , la macroconstante HAUTEUR est remplacée par sa définition : LONGUEUR * 2 . Or, comme nous sommes en train de substituer la macroconstante LONGUEUR , LONGUEUR sera laissé tel quel. Le même raisonnement peut être appliqué pour la macroconstante HAUTEUR .

#define LONGUEUR (HAUTEUR / 2)
#define HAUTEUR (LONGUEUR * 2)

Définition sur plusieurs lignes

La définition d’une macro doit normalement tenir sur une seule ligne. C’est-à-dire que dès que la fin de ligne est atteinte, la définition est considérée comme terminée. Ainsi, dans le code suivant, la définition de la macroconstante BONJOUR_AUREVOIR ne comprend en fait que puts("Bonjour"); .

#define BONJOUR_AUREVOIR puts("Bonjour");
puts("Au revoir")

Pour que cela fonctionne, nous sommes donc contraints de la rédiger comme ceci.

#define BONJOUR_AUREVOIR puts("Bonjour"); puts("Au revoir")

Ce qui est assez peu lisible et peu pratique si notre définition doit comporter une multitude d’instructions ou, pire, des blocs d’instructions… Heureusement, il est possible d’indiquer au préprocesseur que plusieurs lignes doivent être fusionnées. Cela se fait à l’aide du symbole \ , que nous plaçons en fin de ligne.

#define BONJOUR_AUREVOIR puts("Bonjour");\
puts("Au revoir")

Lorsque le préprocesseur rencontre une barre oblique inverse ( \ ) suivie d’une fin de ligne, celles-ci sont supprimées et la ligne qui suit est fusionnée avec la ligne courante.

Notez bien qu' aucun espace n’est ajouté durant l’opération !

#define BONJOUR_AUREVOIR\
puts("Bonjour");\
puts("Au revoir")

Le code ci-dessus est donc incorrect car il donne en vérité ceci.

#define BONJOUR_AUREVOIRputs("Bonjour");puts("Au revoir")

L’utilisation de la fusion ne se limite pas aux définitions de macros, il est possible de l’employer n’importe où dans le code source. Bien que cela ne soit pas obligatoire (une instruction pouvant être répartie sur plusieurs lignes), il est possible d’y recourir pour couper une ligne un peu trop longue tout en indiquant cette césure de manière claire.

printf("Une phrase composée de plusieurs résultats à présenter : %d, %f, %f, %d, %d\n",\
a, b, c, d, e, f);

Si vous souhaitez inclure un bloc d’instructions au sein d’une définition, ne perdez pas de vue que celui-ci constitue une instruction et donc que vous retomberez sur le problème exposé plus haut.

#include <stdio.h>
#define BONJOUR\
    {\
        puts("Bonjour !");\
    }
#define AUREVOIR\
    {\
        puts("Au revoir !");\
    }
int main(void)
{
    if (1)
        BONJOUR; /* erreur ! */
    else
        AUREVOIR;
    return 0;
}

Une solution fréquente à ce problème consiste à recourir à une boucle do {} while avec une condition nulle (de sorte qu’elle ne soit exécutée qu’une seule fois), celle-ci ne constituant qu’une seule instruction avec le point-virgule final.

#include <stdio.h>
#define BONJOUR\
    do {\
        puts("Bonjour !");\
    } while(0)
#define AUREVOIR\
    do {\
        puts("Au revoir !");\
    } while(0)
int main(void)
{
    if (1)
        BONJOUR; /* ok */
    else
        AUREVOIR;
    return 0;
}

Définition nulle

Sachez que le corps d’une macro peut parfaitement être vide.

#define MACRO

Dans un tel cas, la macro ne sera tout simplement pas substituée par quoi que ce soit. Bien que cela puisse paraître étrange, cette technique est souvent utilisée, notamment pour la compilation conditionnelle que nous verrons bientôt.

Annuler une définition

Enfin, une définition peut être annulée à l’aide de la directive #undef en lui spécifiant le nom de la macro qui doit être détruite.

#define TAILLE 100
#undef TAILLE
/* TAILLE n'est à présent plus utilisable. */

Les macrofonctions

Pour l’instant, nous n’avons manipulé que des macroconstantes, c’est-à-dire des macros n’employant pas de paramètres. Comme vous vous en doutez, une macrofonction est une macro qui accepte des paramètres et les emploie dans sa définition.

#define MACRO(paramètre, autre_paramètre) définition

Pour ce faire, le nom de la macro est suivi de parenthèses comprenant le nom des paramètres séparés par une virgule. Chacun d’eux peut ensuite être utilisé dans la définition de la macrofonction et sera remplacé par la suite fournie en argument lors de l’appel à la macrofonction.

Notez bien que nous n’avons pas parlé de « valeur » pour les arguments. En effet, n’importe quelle suite de symboles peut-être passée en argument d’une macrofonction, y compris du code. C’est d’ailleurs ce qui fait la puissance du préprocesseur.

Illustrons ce nouveau concept avec un exemple : nous allons écrire deux macros : EUR qui convertira une somme en euro en francs (français) et FRF qui fera l’inverse. Pour rappel, un euro équivaut à 6,55957 francs français.

Résultat (Afficher/Masquer)
#include <stdio.h>
#define EUR(x) ((x) / 6.55957)
#define FRF(x) ((x) * 6.55957)
int main(void)
{
   printf("Dix francs français valent %f euros.\n", EUR(10));
   printf("Dix euros valent %f francs français.\n", FRF(10));
   return 0;
}

Dix francs français valent 1.524490 euros.
Dix euros valent 65.595700 francs français.

Appliquons encore ce concept avec un deuxième exercice : essayez de créer la macro MIN qui renvoie le minimum entre deux nombres.

Résultat (Afficher/Masquer)
#include <stdio.h>
#define MIN(a, b)  ((a) < (b) ? (a) : (b))
int main(void)
{
   printf("Le minimum entre 16 et 32 est %d.\n", MIN(16, 32));
   printf("Le minimum entre 2+9+7 et 3*8 est %d.\n", MIN(2+9+7, 3*8));
   return 0;
}

Le minimum entre 16 et 32 est 16.
Le minimum entre 2+9+7 et 3*8 est 18.

Remarquez que nous avons utilisé des expressions composées lors de la deuxième utilisation de la macrofonction MIN .

Priorité des opérations

Quelque chose vous a peut-être frappé dans les corrections : pourquoi écrire (x) et pas simplement x ?

En fait, il s’agit d’une protection en vue d’éviter certaines ambiguïtés. En effet, si l’on n’y prend pas garde, on peut par exemple avoir des surprises dues à la priorité des opérateurs. Prenons l’exemple d’une macro MUL qui effectue une multiplication.

#define MUL(a, b)  (a * b)

Tel quel, le code peut poser des problèmes. En effet, si nous appelons la macrofonction comme ceci.

MUL(2+3, 4+5)

Nous obtenons comme résultat 19 (la macro sera remplacée par 2 + 3 * 4 + 5 ) et non 45, qui est le résultat attendu. Pour garantir la bonne marche de la macrofonction, nous devons rajouter des parenthèses.

#define MUL(a, b)  ((a) * (b))

Dans ce cas, nous obtenons bien le résultat souhaité, c’est-à-dire 45 ( (2 + 3) * (4 + 5) ).

Nous vous conseillons de rajouter des parenthèses en cas de doute pour éviter toute erreur.

Les effets de bords

Pour finir, une petite mise en garde : évitez d’utiliser plus d’une fois un paramètre dans la définition d’une macro en vue d’éviter de multiplier d’éventuels effets de bord.

Des effets de quoi ?

Un effet de bord est une modification du contexte d’exécution. Vous voilà bien avancé nous direz-vous… En fait, vous en avez déjà rencontré, l’exemple le plus typique étant une affectation.

a = 10;

Dans cet exemple, le contexte d’exécution du programme (qui comprend ses variables) est modifié puisque la valeur d’une variable est changée. Ainsi, imaginez que la macro MUL soit appelée comme suit.

MUL(a = 10, a = 20)

Après remplacement, celle-ci donnerait l’expression suivante : ((a = 10) * (a = 20)) qui est assez problématique… En effet, quelle sera la valeur de a , finalement ? 10 ou 20 ? Ceci est impossible à dire sans fixer une règle d’évaluation et… la norme n’en prévoit aucune dans ce cas ci. :-°

Aussi, pour éviter ce genre de problèmes tordus, veillez à n’utiliser chaque paramètre qu' une seule fois .

Les directives conditionnelles

Le préprocesseur dispose de cinq directives conditionnelles : #if , #ifdef , #ifndef , #elif et #else . Ces dernières permettent de conserver ou non une potion de code en fonction de la validité d’une condition (si la condition est vraie, le code est gardé sinon il est passé). Chacune de ces directives (ou suite de directives) doit être terminée par une directive #endif .

Les directives #if, #elif et #else

Les directives #if et #elif

Les directives #if et #elif , comme l’instruction if , attendent une expression conditionnelle ; toutefois, celles-ci sont plus restreintes étant donné que nous sommes dans le cadre du préprocesseur. Ce dernier se contentant d’effectuer des substitutions, il n’a par exemple aucune connaissance des mots-clés du langage C ou des variables qui sont employées.

Ainsi, les conditions ne peuvent comporter que des expressions entières (ce qui exclut les nombres flottants et les pointeurs) et constantes (ce qui exclut l’utilisation d’opérateurs à effets de bord comme = ). Aussi, les mots-clés et autres identificateurs (hormis les macros) présents dans la définition sont ignorés (plus précisément, ils sont remplacés par 0 ).

L’exemple ci-dessous explicite cela.

#if 1.89 > 1.88 /* Incorrect, ce ne sont pas des entiers */
#if sizeof(int) == 4 /* Équivalent à 0(0) == 4, `sizeof' et `int' étant des mots-clés */
#if (a = 20) == 20 /* Équivalent à (0 = 20) == 4, `a' étant un identificateur */

Techniquement, seuls les opérateurs suivants peuvent être utilisés : + (binaire ou unaire), - (binaire ou unaire), * , / , % , == , != , <= , < , > , >= , ! , && , || , , , l’opérateur ternaire et les opérateurs de manipulations des bits , que nous verrons au chapitre suivant.

Bien entendu, vous pouvez également utiliser des parenthèses afin de régler la priorité des opérations.

L’opérateur defined

Le préprocesseur fournit un opérateur supplémentaire utilisable dans les conditions : defined . Celui-ci prend comme opérande un nom de macro et retourne 1 ou 0 suivant que ce nom corresponde ou non à une macro définie.

#if defined TAILLE

Celui-ci est fréquemment utilisé pour produire des programmes portables. En effet, chaque système et chaque compilateur définissent généralement une ou des macroconstantes qui lui sont propres. En vérifiant si une de ces constantes existe, nous pouvons déterminer sur quelle plate-forme et avec quel compilateur nous compilons et adapter le code en conséquence.

Notez que ces constantes sont propres à un système d’exploitation et/ou compilateur donné, elles ne sont donc pas spécifiées par la norme du langage C.

La directive #else

La directive #else quant à elle, se comporte comme l’instruction éponyme.

Exemple

Voici un exemple d’utilisation.

#include <stdio.h>
#define A 2
int main(void)
{
#if A < 0
    puts("A < 0");
#elif A > 0
    puts("A > 0");
#else
    puts("A == 0");
#endif
    return 0;
}
Résultat
A > 0

Notez que les directives conditionnelles peuvent être utilisées à la place des commentaires en vue d’empêcher la compilation d’un morceau de code. L’avantage de cette technique par rapport aux commentaires est qu’elle vous évite de produire des commentaires imbriqués.

#if 0
/* On multiplie par TAILLE */
variable *= TAILLE;
printf("Variable vaut : %d\n", variable);
#endif

Les directives #ifdef et #ifndef

Ces deux directives sont en fait la contraction, respectivement, de la directive #if defined et #if !defined . Si vous n’avez qu’une seule constante à tester, il est plus rapide d’utiliser ces deux directives à la place de leur version longue.

Protection des fichiers d’en-tête

Cela étant dit, vous devriez à présent être en mesure de comprendre le fonctionnement des directives jusqu’à présent utilisées dans les fichiers en-têtes.

#ifndef CONSTANTE_H
#define CONSTANTE_H
/* Les déclarations */
#endif

La première directive vérifie que la macroconstante CONSTANTE_H n’a pas encore été définie. Si ce n’est pas le cas, elle est définie (ici avec une valeur nulle) et le bloc de la condition (comprenant le contenu du fichier d’en-tête) est inclus. Si elle a déjà été définie, le bloc est sauté et le contenu du fichier n’est ainsi pas à nouveau inclus.

Pour rappel, ceci est nécessaire pour éviter des problèmes d’inclusions multiples, par exemple lorsqu’un fichier A inclut un fichier B, qui lui-même inclut le fichier A.

Avec ce chapitre, vous devriez pouvoir utiliser le préprocesseur de manière basique et éviter plusieurs de ses pièges fréquents. :)

En résumé

  • La directive #include permet d’inclure le contenu d’un fichier dans un autre ;
  • La directive #define permet de définir des macros qui sont des substituts à des morceaux de code ;
  • Les macros sans paramètres sont appelées macroconstantes ;
  • Les macros qui acceptent des paramètres et les emploient dans leur définition sont appelées des macrofonctions ;
  • Des effets de bord sont possibles si un paramètre est utilisé plus d’une fois dans la définition d’une macrofonction ;
  • Grâce aux directives conditionnelles, il est possible de conserver ou non une potion de code, en fonction de conditions.


TP : un Puissance 4 - Le langage C • Tutoriels • Zeste de Savoir

TP : un Puissance 4

Première étape : le jeu

Dans cette première partie, vous allez devoir réaliser un « Puissance 4 » pour deux joueurs humains . Le programme final devra ressembler à ceci.

  1   2   3   4   5   6   7
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
  1   2   3   4   5   6   7
Joueur 1 : 1
  1   2   3   4   5   6   7
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
| O |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
  1   2   3   4   5   6   7
Joueur 2 : 7
  1   2   3   4   5   6   7
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   |   |   |   |   |
+---+---+---+---+---+---+---+
| O |   |   |   |   |   | X |
+---+---+---+---+---+---+---+
  1   2   3   4   5   6   7
Joueur 1 :

Pour ceux qui ne connaissent pas le jeu Puissance 4, ce dernier se joue à l’aide d’une grille verticale de sept colonnes sur six lignes. Chaque joueur dispose de vingt et un jetons d’une couleur (le plus souvent, rouge et jaune traduit dans notre exemple par les caractères « O » et « X ») et place ceux-ci au sein de la grille à tour de rôle.

Pour gagner le jeu, un joueur doit aligner quatre jetons verticalement, horizontalement ou en oblique. Il s’agit donc du même principe que le Morpion à une différence prêt : la grille est verticale ce qui signifie que les jetons tombent au fond de la colonne choisie par le joueur. Pour plus de détails, je vous renvoie à l’article dédié sur Wikipédia.

Maintenant que vous savez cela, il est temps de passer à la réalisation de celui-ci.
Bon travail ! ;)

Correction

Bien, l’heure de la correction est venue. :)

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#define P4_COLONNES (7)
#define P4_LIGNES (6)
#define J1_JETON ('O')
#define J2_JETON ('X')
#define ACT_ERR (0)
#define ACT_JOUER (1)
#define ACT_NOUVELLE_SAISIE (2)
#define ACT_QUITTER (3)
#define STATUT_OK (0)
#define STATUT_GAGNE (1)
#define STATUT_EGALITE (2)
struct position
{
    int colonne;
    int ligne;
};
static void affiche_grille(void);
static void calcule_position(int, struct position *);
static unsigned calcule_nb_jetons_depuis_vers(struct position *, int, int, char);
static unsigned calcule_nb_jetons_depuis(struct position *, char);
static int coup_valide(int);
static int demande_action(int *);
static int grille_complete(void);
static void initialise_grille(void);
static int position_valide(struct position *);
static int statut_jeu(struct position *pos, char);
static unsigned umax(unsigned, unsigned);
static int vider_tampon(FILE *);
static char grille[P4_COLONNES][P4_LIGNES];
static void affiche_grille(void)
{
    /*
     * Affiche la grille pour le ou les joueurs.
     */
    int col;
    int lgn;
    putchar('\n');
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("  %d ", col);
    putchar('\n');
    putchar('+');
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("---+");
    putchar('\n');
    for (lgn = 0; lgn < P4_LIGNES; ++lgn)
    {
        putchar('|');
        for (col = 0; col < P4_COLONNES; ++col)
            if (isalpha(grille[col][lgn]))
                printf(" %c |", grille[col][lgn]);
            else
                printf(" %c |", ' ');
        putchar('\n');
        putchar('+');
        for (col = 1; col <= P4_COLONNES; ++col)
            printf("---+");
        putchar('\n');
    }
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("  %d ", col);
    putchar('\n');
}
static void calcule_position(int coup, struct position *pos)
{
    /*
     * Traduit le coup joué en un numéro de colonne et de ligne.
     */
    int lgn;
    pos->colonne = coup;
    for (lgn = P4_LIGNES - 1; lgn >= 0; --lgn)
        if (grille[pos->colonne][lgn] == ' ')
        {
            pos->ligne = lgn;
            break;
        }
}
static unsigned calcule_nb_jetons_depuis_vers(struct position *pos, int dpl_hrz, int dpl_vrt, char jeton)
{
    /*
     * Calcule le nombre de jetons adajcents identiques depuis une position donnée en se
     * déplaçant de `dpl_hrz` horizontalement et `dpl_vrt` verticalement.
     * La fonction s'arrête si un jeton différent ou une case vide est rencontrée ou si
     * les limites de la grille sont atteintes.
     */
    struct position tmp;
    unsigned nb = 1;
    tmp.colonne = pos->colonne + dpl_hrz;
    tmp.ligne = pos->ligne + dpl_vrt;
    while (position_valide(&tmp))
    {
        if (grille[tmp.colonne][tmp.ligne] == jeton)
            ++nb;
        else
            break;
        tmp.colonne += dpl_hrz;
        tmp.ligne += dpl_vrt;
    }
    return nb;
}
static unsigned calcule_nb_jetons_depuis(struct position *pos, char jeton)
{
    /*
     * Calcule le nombre de jetons adjacents en vérifant la colonne courante,
     * de la ligne courante et des deux obliques courantes.
     * Pour ce faire, la fonction calcule_nb_jeton_depuis_vers() est appelé à
     * plusieurs reprises afin de parcourir la grille suivant la vérification
     * à effectuer.
     */
    unsigned max;
    max = calcule_nb_jetons_depuis_vers(pos, 0, 1, jeton);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, 0, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, 0, jeton) - 1);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, 1, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, -1, jeton) - 1);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, -1, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, 1, jeton) - 1);
    return max;
}
static int coup_valide(int col)
{
    /*
     * Si la colonne renseignée est inférieure ou égal à zéro
     * ou que celle-ci est supérieure à la longueur du tableau
     * ou que la colonne indiquée est saturée
     * alors le coup est invalide.
     */
    if (col <= 0 || col > P4_COLONNES || grille[col - 1][0] != ' ')
        return 0;
    return 1;
}
static int demande_action(int *coup)
{
    /*
     * Demande l'action à effectuer au joueur courant.
     * S'il entre un chiffre, c'est qu'il souhaite jouer.
     * S'il entre la lettre « Q » ou « q », c'est qu'il souhaite quitter.
     * S'il entre autre chose, une nouvelle saisie sera demandée.
     */
    char c;
    int ret = ACT_ERR;
    if (scanf("%d", coup) != 1)
    {
        if (scanf("%c", &c) != 1)
        {
            fprintf(stderr, "Erreur lors de la saisie\n");
            return ret;
        }
        switch (c)
        {
        case 'Q':
        case 'q':
            ret = ACT_QUITTER;
            break;
        default:
            ret = ACT_NOUVELLE_SAISIE;
            break;
        }
    }
    else
        ret = ACT_JOUER;
    if (!vider_tampon(stdin))
    {
         fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
         ret = ACT_ERR;
    }
    return ret;
}
static int grille_complete(void)
{
    /*
     * Détermine si la grille de jeu est complète.
     */
    unsigned col;
    unsigned lgn;
    for (col = 0; col < P4_COLONNES; ++col)
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
            if (grille[col][lgn] == ' ')
                return 0;
    return 1;
}
static void initialise_grille(void)
{
    /*
     * Initalise les caractères de la grille.
     */
    unsigned col;
    unsigned lgn;
    for (col = 0; col < P4_COLONNES; ++col)
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
            grille[col][lgn] = ' ';
}
static int position_valide(struct position *pos)
{
    /*
     * Vérifie que la position fournie est bien comprise dans la grille.
     */
    int ret = 1;
    if (pos->colonne >= P4_COLONNES || pos->colonne < 0)
        ret = 0;
    else if (pos->ligne >= P4_LIGNES || pos->ligne < 0)
        ret = 0;
    return ret;
}
static int statut_jeu(struct position *pos, char jeton)
{
    /*
     * Détermine s'il y a lieu de continuer le jeu ou s'il doit être
     * arrêté parce qu'un joueur a gagné ou que la grille est complète.
     */
    if (grille_complete())
        return STATUT_EGALITE;
    else if (calcule_nb_jetons_depuis(pos, jeton) >= 4)
        return STATUT_GAGNE;
    return STATUT_OK;
}
static unsigned umax(unsigned a, unsigned b)
{
    /*
     * Retourne le plus grand des deux arguments.
     */
    return (a > b) ? a : b;
}
static int vider_tampon(FILE *fp)
{
    /*
     * Vide les données en attente de lecture du flux spécifié.
     */
    int c;
    do
        c = fgetc(fp);
    while (c != '\n' && c != EOF);
    return ferror(fp) ? 0 : 1;
}
int main(void)
{
    int statut;
    char jeton = J1_JETON;
    initialise_grille();
    affiche_grille();
    while (1)
    {
        struct position pos;
        int action;
        int coup;
        printf("Joueur %d : ", (jeton == J1_JETON) ? 1 : 2);
        action = demande_action(&coup);
        if (action == ACT_ERR)
            return EXIT_FAILURE;
        else if (action == ACT_QUITTER)
            return 0;
        else if (action == ACT_NOUVELLE_SAISIE || !coup_valide(coup))
        {
            fprintf(stderr, "Vous ne pouvez pas jouer à cet endroit\n");
            continue;
        }
        calcule_position(coup - 1, &pos);
        grille[pos.colonne][pos.ligne] = jeton;
        affiche_grille();
        statut = statut_jeu(&pos, jeton);
        if (statut != STATUT_OK)
            break;
        jeton = (jeton == J1_JETON) ? J2_JETON : J1_JETON;
    }
    if (statut == STATUT_GAGNE)
        printf("Le joueur %d a gagné\n", (jeton == J1_JETON) ? 1 : 2);
    else if (statut == STATUT_EGALITE)
        printf("Égalité\n");
    return 0;
}

Le programme commence par initialiser la grille (tous les caractères la composant sont des espaces au début, symbolisant une case vide) et l’afficher.

Ensuite, il est demandé au premier joueur d’effectuer une action. Cela est réalisé via la fonction demande_action() qui attend du joueur soit qu’il entre un numéro de colonne, soit la lettre Q ou q . Si le joueur entre autre chose ou que la colonne indiquée est invalide, une nouvelle saisie lui est demandée.

Dans le cas où le numéro de colonne est valable, la fonction calcule_position() est appelée afin de calculer le numéro de ligne et de stocker celui-ci et le numéro de colonne dans une structure. Après quoi la grille est mise à jour en ajoutant un nouveau jeton et à nouveau affichée.

La fonction statut_jeu() entre alors en action et détermine si quatre jetons adjacents sont présents ou non ou si la grille est complète. Cela est notamment réalisé à l’aide de la fonction calcule_nb_jetons_depuis() qui fait elle-même appel à la fonction calcule_nb_jetons_depuis_vers() . Si aucune des deux conditions n’est remplie, c’est reparti pour un tour sinon la boucle est quittée et le programme se termine.

La fonction calcule_nb_jetons_depuis_vers() mérite que l’on s’attarde un peu sur elle. Celle-ci reçoit une position dans la grille et se déplace suivant les valeurs attribuées à dpl_hrz (soit « déplacement horizontal ») et dpl_vrt (pour « déplacement vertical »). Celle-ci effectue son parcours aussi longtemps que des jetons identiques sont rencontrés et que le déplacement a lieu dans la grille. Elle retourne ensuite le nombre de jetons identiques rencontrés (notez que nous commençons le décompte à un puisque, par définition, la position indiquée est celle qui vient d’être jouée par un des joueurs).

La fonction calcule_nb_jetons_depuis() appelle à plusieurs reprises la fonction calcule_nb_jetons_depuis_vers() afin d’obtenir le nombre de jetons adjacents de la colonne courante (déplacement de un vers le bas), de la ligne (deux déplacements : un vers la gauche et un vers la droite) et des deux obliques (deux déplacements pour chacune d’entre elles).

Notez qu’afin d’éviter de nombreux passages d’arguments, nous avons employé la variable globale grille (dont le nom est explicite).

Nous insistons sur le fait que nous vous présentons une solution parmi d’autres. Ce n’est pas parce que votre façon de procéder est différente qu’elle est incorrecte.

Deuxième étape : une petite IA

Introduction

Dans cette seconde partie, votre objectif va être de construire une petite intelligence artificielle. Votre programme va donc devoir demander si un ou deux joueurs sont présents et jouer à la place du second joueur s’il n’y en a qu’un seul.

Afin de vous aider dans la réalisation de cette tâche, nous allons vous présenter un algorithme simple que vous pourrez mettre en œuvre. Le principe est le suivant : pour chaque emplacement possible (il y en aura toujours au maximum sept), nous allons calculer combien de pièces composeraient la ligne si nous jouions à cet endroit sans tenir compte de notre couleur (autrement dit, le jeton que nous jouons est considéré comme étant des deux couleurs). Si une valeur est plus élevée que les autres, l’emplacement correspondant sera choisi. Si en revanche il y a plusieurs nombres égaux, une des cases sera choisie « au hasard » (nous y reviendrons).

Cet algorithme connait toutefois une exception : s’il s’avère lors de l’analyse qu’un coup permet à l’ordinateur de gagner, alors le traitement s’arrêtera là et le mouvement est immédiatement joué.

Par exemple, dans la grille ci-dessous, il est possible de jouer à sept endroits (indiqués à l’aide d’un point d’interrogation).

  1   2   3   4   5   6   7
+---+---+---+---+---+---+---+
|   |   |   | ? |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   | O | ? |   |   |
+---+---+---+---+---+---+---+
|   |   |   | O | X | ? |   |
+---+---+---+---+---+---+---+
|   |   |   | O | O | X |   |
+---+---+---+---+---+---+---+
|   | ? | ? | X | O | O | ? |
+---+---+---+---+---+---+---+
| ? | X | O | O | X | X | X |
+---+---+---+---+---+---+---+
  1   2   3   4   5   6   7

Si nous appliquons l’algorithme que nous venons de décrire, nous obtenons les valeurs suivantes.

  1   2   3   4   5   6   7
+---+---+---+---+---+---+---+
|   |   |   | 4 |   |   |   |
+---+---+---+---+---+---+---+
|   |   |   | O | 2 |   |   |
+---+---+---+---+---+---+---+
|   |   |   | O | X | 2 |   |
+---+---+---+---+---+---+---+
|   |   |   | O | O | X |   |
+---+---+---+---+---+---+---+
|   | 2 | 2 | X | O | O | 3 |
+---+---+---+---+---+---+---+
| 2 | X | O | O | X | X | X |
+---+---+---+---+---+---+---+
  1   2   3   4   5   6   7

Le programme jouera donc dans la colonne quatre, cette dernière ayant la valeur la plus élevée (ce qui tombe bien puisque l’autre joueur gagnerait si l’ordinateur jouait ailleurs :p ).

Pour effectuer le calcul, vous pouvez vous aider des fonctions long_colonne() , long_ligne() et long_oblique() de la correction précédente.

Tirage au sort

Nous vous avons dit que si l’ordinateur doit choisir entre des valeurs identiques, ce dernier devrait en choisir une « au hasard ». Cependant, un ordinateur est en vérité incapable d’effectuer cette tâche (c’est pour cela que nous employons des guillemets). Pour y remédier, il existe des algorithmes qui permettent de produire des suites de nombres dits pseudo-aléatoires , c’est-à-dire qui s’approchent de suites aléatoires sur le plan statistique.

La plupart de ceux-ci fonctionnent à partir d’une graine , c’est-à-dire un nombre de départ qui va déterminer tout le reste de la suite. C’est ce système qui a été choisi par la bibliothèque standard du C. Deux fonctions vous sont dès lors proposées : srand() et rand() (elles sont déclarées dans l’en-tête <stdlib.h> ).

void srand(unsigned int seed);
int rand(void);

La fonction srand() est utilisée pour initaliser la génération de nombres à l’aide de la graine fournie en argument (un entier non signé en l’espèce). Le plus souvent vous ne l’appellerez qu’une seule fois au début de votre programme sauf si vous souhaitez réinitialiser la génération de nombres.

La fonction rand() retourne un nombre pseudo-aléatoire compris entre zéro et RAND_MAX (qui est une constante également définie dans l’en-tête <stdlib.h> ) suivant la graine fournie à la fonction srand() .

Toutefois, il y a un petit bémol : étant donné que l’algorithme de génération se base sur la graine fournie pour construire la suite de nombres, une même graine donnera toujours la même suite ! Dès lors, comment faire pour que les nombres générés ne soient pas identiques entre deux exécutions du programme ? Pour que cela soit possible, nous devrions fournir un nombre différent à srand() à chaque fois, mais comment obtenir un tel nombre ?

C’est ici que la fonction time() (déclarée dans l’en-tête <time.h> ) entre en jeu.

time_t time(time_t *t);

La fonction time() retourne la date actuelle sous forme d’une valeur de type time_t (qui est un nombre entier ou flottant). Le plus souvent, il s’agit du nombre de secondes écoulé depuis une date fixée arbitrairement appelée Epoch en anglais. Cette valeur peut également être stockée dans une variable de type time_t dont l’adresse lui est fournie en argument (un pointeur nul peut lui être envoyé si cela n’est pas nécessaire). En cas d’erreur, la fonction retourne la valeur -1 convertie vers le type time_t .

Pour obtenir des nombres différents à chaque exécution, nous pouvons donc appeler srand() avec le retour de la fonction time() converti en entier non signé. L’exemple suivant génère donc une suite de trois nombres pseudo-aléatoires différents à chaque exécution (en vérité à chaque exécution espacée d’une seconde).

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
    time_t t;
    if (time(&t) == (time_t)-1)
    {
        fprintf(stderr, "Impossible d'obtenir la date courante\n");
        return EXIT_FAILURE;
    }
    srand((unsigned)t);
    printf("1 : %d\n", rand());
    printf("2 : %d\n", rand());
    printf("3 : %d\n", rand());
    return 0;
}

Tirer un nombre dans un intervalle donné

Il est possible de générer des nombres dans un intervalle précis en procédant en deux temps. Tout d’abord, il nous est nécessaire d’obtenir un nombre compris entre zéro inclus et un exclus. Pour ce faire, nous pouvons diviser le nombre pseudo-aléatoire obtenu par la plus grande valeur qui peut être retournée par la fonction rand() augmentée de un. Celle-ci nous est fournie via la macroconstante RAND_MAX qui est définie dans l’en-tête <stdlib.h> .

double nb_aleatoire(void)
{
    return rand() / (RAND_MAX + 1.);
}

Notez que nous avons bien indiqué que la constante 1. est un nombre flottant afin que le résultat de l’opération soit de type double .

Ensuite, il nous suffit de multiplier le nombre obtenu par la différence entre le maximum et le minimum augmenté de un et d’y ajouter le minimum.

int nb_aleatoire_entre(int min, int max)
{
    return nb_aleatoire() * (max - min + 1) + min;
}

Afin d’illustrer ce qui vient d’être dit, le code suivant affiche trois nombres pseudo-aléatoires compris entre zéro et dix inclus.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
static double nb_aleatoire(void)
{
    return rand() / (RAND_MAX + 1.);
}
static int nb_aleatoire_entre(int min, int max)
{
    return nb_aleatoire() * (max - min + 1) + min;
}
int main(void)
{
    time_t t;
    if (time(&t) == (time_t)-1)
    {
        fprintf(stderr, "Impossible d'obtenir la date courante\n");
        return EXIT_FAILURE;
    }
    srand((unsigned)t);
    printf("1 : %d\n", nb_aleatoire_entre(0, 10));
    printf("2 : %d\n", nb_aleatoire_entre(0, 10));
    printf("3 : %d\n", nb_aleatoire_entre(0, 10));
    return 0;
}

À présent, c’est à vous ! :)

Correction

#include <ctype.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#define P4_COLONNES (7)
#define P4_LIGNES (6)
#define J1_JETON ('O')
#define J2_JETON ('X')
#define ACT_ERR (0)
#define ACT_JOUER (1)
#define ACT_NOUVELLE_SAISIE (2)
#define ACT_QUITTER (3)
#define STATUT_OK (0)
#define STATUT_GAGNE (1)
#define STATUT_EGALITE (2)
struct position
{
    int colonne;
    int ligne;
};
static void affiche_grille(void);
static void calcule_position(int, struct position *);
static unsigned calcule_nb_jetons_depuis_vers(struct position *, int, int, char);
static unsigned calcule_nb_jetons_depuis(struct position *, char);
static int coup_valide(int);
static int demande_action(int *);
static int demande_nb_joueur(void);
static int grille_complete(void);
static int ia(void);
static void initialise_grille(void);
double nb_aleatoire(void);
int nb_aleatoire_entre(int, int);
static int position_valide(struct position *);
static int statut_jeu(struct position *pos, char);
static unsigned umax(unsigned, unsigned);
static int vider_tampon(FILE *);
static char grille[P4_COLONNES][P4_LIGNES];
static void affiche_grille(void)
{
    /*
     * Affiche la grille pour le ou les joueurs.
     */
    int col;
    int lgn;
    putchar('\n');
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("  %d ", col);
    putchar('\n');
    putchar('+');
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("---+");
    putchar('\n');
    for (lgn = 0; lgn < P4_LIGNES; ++lgn)
    {
        putchar('|');
        for (col = 0; col < P4_COLONNES; ++col)
            if (isalpha(grille[col][lgn]))
                printf(" %c |", grille[col][lgn]);
            else
                printf(" %c |", ' ');
        putchar('\n');
        putchar('+');
        for (col = 1; col <= P4_COLONNES; ++col)
            printf("---+");
        putchar('\n');
    }
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("  %d ", col);
    putchar('\n');
}
static void calcule_position(int coup, struct position *pos)
{
    /*
     * Traduit le coup joué en un numéro de colonne et de ligne.
     */
    int lgn;
    pos->colonne = coup;
    for (lgn = P4_LIGNES - 1; lgn >= 0; --lgn)
        if (grille[pos->colonne][lgn] == ' ')
        {
            pos->ligne = lgn;
            break;
        }
}
static unsigned calcule_nb_jetons_depuis_vers(struct position *pos, int dpl_hrz, int dpl_vrt, char jeton)
{
    /*
     * Calcule le nombre de jetons adajcents identiques depuis une position donnée en se
     * déplaçant de `dpl_hrz` horizontalement et `dpl_vrt` verticalement.
     * La fonction s'arrête si un jeton différent ou une case vide est rencontrée ou si
     * les limites de la grille sont atteintes.
     */
    struct position tmp;
    unsigned nb = 1;
    tmp.colonne = pos->colonne + dpl_hrz;
    tmp.ligne = pos->ligne + dpl_vrt;
    while (position_valide(&tmp))
    {
        if (grille[tmp.colonne][tmp.ligne] == jeton)
            ++nb;
        else
            break;
        tmp.colonne += dpl_hrz;
        tmp.ligne += dpl_vrt;
    }
    return nb;
}
static unsigned calcule_nb_jetons_depuis(struct position *pos, char jeton)
{
    /*
     * Calcule le nombre de jetons adjacents en vérifant la colonne courante,
     * de la ligne courante et des deux obliques courantes.
     * Pour ce faire, la fonction calcule_nb_jeton_depuis_vers() est appelé à
     * plusieurs reprises afin de parcourir la grille suivant la vérification
     * à effectuer.
     */
    unsigned max;
    max = calcule_nb_jetons_depuis_vers(pos, 0, 1, jeton);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, 0, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, 0, jeton) - 1);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, 1, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, -1, jeton) - 1);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, -1, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, 1, jeton) - 1);
    return max;
}
static int coup_valide(int col)
{
    /*
     * Si la colonne renseignée est inférieure ou égale à zéro
     * ou que celle-ci est supérieure à la longueur du tableau
     * ou que la colonne indiquée est saturée
     * alors le coup est invalide.
     */
    if (col <= 0 || col > P4_COLONNES || grille[col - 1][0] != ' ')
        return 0;
    return 1;
}
static int demande_action(int *coup)
{
    /*
     * Demande l'action à effectuer au joueur courant.
     * S'il entre un chiffre, c'est qu'il souhaite jouer.
     * S'il entre la lettre « Q » ou « q », c'est qu'il souhaite quitter.
     * S'il entre autre chose, une nouvelle saisie sera demandée.
     */
    char c;
    int ret = ACT_ERR;
    if (scanf("%d", coup) != 1)
    {
        if (scanf("%c", &c) != 1)
        {
            fprintf(stderr, "Erreur lors de la saisie\n");
            return ret;
        }
        switch (c)
        {
        case 'Q':
        case 'q':
            ret = ACT_QUITTER;
            break;
        default:
            ret = ACT_NOUVELLE_SAISIE;
            break;
        }
    }
    else
        ret = ACT_JOUER;
    if (!vider_tampon(stdin))
    {
         fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
         ret = ACT_ERR;
    }
    return ret;
}
static int demande_nb_joueur(void)
{
    /*
     * Demande et récupère le nombre de joueurs.
     */
    int njoueur = 0;
    while (1)
    {
        printf("Combien de joueurs prennent part à cette partie ? ");
        if (scanf("%d", &njoueur) != 1 && ferror(stdin))
        {
                fprintf(stderr, "Erreur lors de la saisie\n");
                return 0;
        }
        else if (njoueur != 1 && njoueur != 2)
            fprintf(stderr, "Plait-il ?\n");
        else
            break;
        if (!vider_tampon(stdin))
        {
            fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
            return 0;
        }
    }
    return njoueur;
}
static int grille_complete(void)
{
    /*
     * Détermine si la grille de jeu est complète.
     */
    unsigned col;
    unsigned lgn;
    for (col = 0; col < P4_COLONNES; ++col)
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
            if (grille[col][lgn] == ' ')
                return 0;
    return 1;
}
static int ia(void)
{
    /*
     * Fonction mettant en œuvre l'IA présentée.
     * Assigne une valeur pour chaque colonne libre et retourne ensuite le numéro de
     * colonne ayant la plus haute valeur. Dans le cas où plusieurs valeurs égales sont
     * générées, un numéro de colonne est « choisi au hasard » parmi celles-ci.
     */
    unsigned meilleurs_col[P4_COLONNES];
    unsigned nb_meilleurs_col = 0;
    unsigned max = 0;
    unsigned col;
    for (col = 0; col < P4_COLONNES; ++col)
    {
        struct position pos;
        unsigned longueur;
        if (grille[col][0] != ' ')
            continue;
        calcule_position(col, &pos);
        longueur = calcule_nb_jetons_depuis(&pos, J2_JETON);
        if (longueur >= 4)
            return col;
        longueur = umax(longueur, calcule_nb_jetons_depuis(&pos, J1_JETON));
        if (longueur >= max)
        {
            if (longueur > max)
            {
                nb_meilleurs_col = 0;
                max = longueur;
            }
            meilleurs_col[nb_meilleurs_col++] = col;
        }
    }
    return meilleurs_col[nb_aleatoire_entre(0, nb_meilleurs_col - 1)];
}
static void initialise_grille(void)
{
    /*
     * Initalise les caractères de la grille.
     */
    unsigned col;
    unsigned lgn;
    for (col = 0; col < P4_COLONNES; ++col)
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
            grille[col][lgn] = ' ';
}
static unsigned umax(unsigned a, unsigned b)
{
    /*
     * Retourne le plus grand des deux arguments.
     */
    return (a > b) ? a : b;
}
double nb_aleatoire(void)
{
    /*
     * Retourne un nombre pseudo-aléatoire compris entre zéro inclus et un exclus.
     */
    return rand() / ((double)RAND_MAX + 1.);
}
int nb_aleatoire_entre(int min, int max)
{
    /*
     * Retourne un nombre pseudo-aléatoire entre `min` et `max` inclus.
     */
    return nb_aleatoire() * (max - min + 1) + min;
}
static int position_valide(struct position *pos)
{
    /*
     * Vérifie que la position fournie est bien comprise dans la grille.
     */
    int ret = 1;
    if (pos->colonne >= P4_COLONNES || pos->colonne < 0)
        ret = 0;
    else if (pos->ligne >= P4_LIGNES || pos->ligne < 0)
        ret = 0;
    return ret;
}
static int statut_jeu(struct position *pos, char jeton)
{
    /*
     * Détermine s'il y a lieu de continuer le jeu ou s'il doit être
     * arrêté parce qu'un joueur a gagné ou que la grille est complète.
     */
    if (grille_complete())
        return STATUT_EGALITE;
    else if (calcule_nb_jetons_depuis(pos, jeton) >= 4)
        return STATUT_GAGNE;
    return STATUT_OK;
}
static int vider_tampon(FILE *fp)
{
    /*
     * Vide les données en attente de lecture du flux spécifié.
     */
    int c;
    do
        c = fgetc(fp);
    while (c != '\n' && c != EOF);
    return ferror(fp) ? 0 : 1;
}
int main(void)
{
    int statut;
    char jeton = J1_JETON;
    int njoueur;
    initialise_grille();
    affiche_grille();
    njoueur = demande_nb_joueur();
    if (!njoueur)
        return EXIT_FAILURE;
    while (1)
    {
        struct position pos;
        int action;
        int coup;
        if (njoueur == 1 && jeton == J2_JETON)
        {
            coup = ia();
            printf("Joueur 2 : %d\n", coup + 1);
            calcule_position(coup, &pos);
        }
        else
        {
            printf("Joueur %d : ", (jeton == J1_JETON) ? 1 : 2);
            action = demande_action(&coup);
            if (action == ACT_ERR)
                return EXIT_FAILURE;
            else if (action == ACT_QUITTER)
                return 0;
            else if (action == ACT_NOUVELLE_SAISIE || !coup_valide(coup))
            {
                fprintf(stderr, "Vous ne pouvez pas jouer à cet endroit\n");
                continue;
            }
            calcule_position(coup - 1, &pos);
        }
        grille[pos.colonne][pos.ligne] = jeton;
        affiche_grille();
        statut = statut_jeu(&pos, jeton);
        if (statut != STATUT_OK)
            break;
        jeton = (jeton == J1_JETON) ? J2_JETON : J1_JETON;
    }
    if (statut == STATUT_GAGNE)
        printf("Le joueur %d a gagné\n", (jeton == J1_JETON) ? 1 : 2);
    else if (statut == STATUT_EGALITE)
        printf("Égalité\n");
    return 0;
}

Le programme demande désormais combien de joueurs sont présents. Dans le cas où ils sont deux, le programme se comporte de la même manière que précédemment. S’il n’y en a qu’un seul, la fonction ia() est appelée lors du tour du deuxième joueur. Cette fonction retourne un numéro de colonne après analyse de la grille. Notez également que les fonctions time() et srand() sont appelées au début afin d’initialiser la génération de nombre pseudo-aléatoires.

La fonction ia() agit comme suit :

  • les numéros des colonnes ayant les plus grandes valeurs calculées sont stockés dans le tableau meilleurs_col ;
  • si la colonne est complète, celle-ci est passée ;
  • si la colonne est incomplète, la fonction calcule_position_depuis() est appelée afin de déterminer combien de pièces seraient adjacentes si un jeton était posé à cet endroit. S’il s’avère que l’ordinateur peut gagner, la fonction retourne immédiatement le numéro de la colonne courante ;
  • un des numéros de colonne présent dans le tableau meilleurs_col est tiré « au hasard » et est retourné.

Troisième et dernière étape : un système de sauvegarde/restauration

Pour terminer, nous allons ajouter un système de sauvegarde/restauration à notre programme.

Durant une partie, un utilisateur doit pouvoir demander à sauvegarder le jeu courant ou de charger une ancienne partie en lieu et place d’entrer un numéro de colonne.

À vous de voir comment organiser les données au sein d’un fichier et comment les récupérés depuis votre programme. ;)

Correction

Afficher/Masquer le contenu masqué
#include <ctype.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define P4_COLONNES (7)
#define P4_LIGNES (6)
#define J1_JETON ('O')
#define J2_JETON ('X')
#define ACT_ERR (0)
#define ACT_JOUER (1)
#define ACT_NOUVELLE_SAISIE (2)
#define ACT_SAUVEGARDER (3)
#define ACT_CHARGER (4)
#define ACT_QUITTER (5)
#define STATUT_OK (0)
#define STATUT_GAGNE (1)
#define STATUT_EGALITE (2)
#define MAX_NOM (255)
struct position
{
    int colonne;
    int ligne;
};
static void affiche_grille(void);
static void calcule_position(int, struct position *);
static unsigned calcule_nb_jetons_depuis_vers(struct position *, int, int, char);
static unsigned calcule_nb_jetons_depuis(struct position *, char);
static int charger_jeu(char *nom, char *, int *);
static int coup_valide(int);
static int demande_action(int *);
static int demande_fichier(char *, size_t);
static int demande_nb_joueur(void);
static int grille_complete(void);
static int ia(void);
static void initialise_grille(void);
double nb_aleatoire(void);
int nb_aleatoire_entre(int, int);
static int position_valide(struct position *);
static int sauvegarder_jeu(char *nom, char, int);
static int statut_jeu(struct position *pos, char);
static unsigned umax(unsigned, unsigned);
static int vider_tampon(FILE *);
static char grille[P4_COLONNES][P4_LIGNES];
static void affiche_grille(void)
{
    /*
     * Affiche la grille pour le ou les joueurs.
     */
    int col;
    int lgn;
    putchar('\n');
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("  %d ", col);
    putchar('\n');
    putchar('+');
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("---+");
    putchar('\n');
    for (lgn = 0; lgn < P4_LIGNES; ++lgn)
    {
        putchar('|');
        for (col = 0; col < P4_COLONNES; ++col)
            if (isalpha(grille[col][lgn]))
                printf(" %c |", grille[col][lgn]);
            else
                printf(" %c |", ' ');
        putchar('\n');
        putchar('+');
        for (col = 1; col <= P4_COLONNES; ++col)
            printf("---+");
        putchar('\n');
    }
    for (col = 1; col <= P4_COLONNES; ++col)
        printf("  %d ", col);
    putchar('\n');
}
static void calcule_position(int coup, struct position *pos)
{
    /*
     * Traduit le coup joué en un numéro de colonne et de ligne.
     */
    int lgn;
    pos->colonne = coup;
    for (lgn = P4_LIGNES - 1; lgn >= 0; --lgn)
        if (grille[pos->colonne][lgn] == ' ')
        {
            pos->ligne = lgn;
            break;
        }
}
static unsigned calcule_nb_jetons_depuis_vers(struct position *pos, int dpl_hrz, int dpl_vrt, char jeton)
{
    /*
     * Calcule le nombre de jetons adajcents identiques depuis une position donnée en se
     * déplaçant de `dpl_hrz` horizontalement et `dpl_vrt` verticalement.
     * La fonction s'arrête si un jeton différent ou une case vide est rencontrée ou si
     * les limites de la grille sont atteintes.
     */
    struct position tmp;
    unsigned nb = 1;
    tmp.colonne = pos->colonne + dpl_hrz;
    tmp.ligne = pos->ligne + dpl_vrt;
    while (position_valide(&tmp))
    {
        if (grille[tmp.colonne][tmp.ligne] == jeton)
            ++nb;
        else
            break;
        tmp.colonne += dpl_hrz;
        tmp.ligne += dpl_vrt;
    }
    return nb;
}
static unsigned calcule_nb_jetons_depuis(struct position *pos, char jeton)
{
    /*
     * Calcule le nombre de jetons adjacents en vérifant la colonne courante,
     * de la ligne courante et des deux obliques courantes.
     * Pour ce faire, la fonction calcule_nb_jeton_depuis_vers() est appelé à
     * plusieurs reprises afin de parcourir la grille suivant la vérification
     * à effectuer.
     */
    unsigned max;
    max = calcule_nb_jetons_depuis_vers(pos, 0, 1, jeton);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, 0, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, 0, jeton) - 1);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, 1, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, -1, jeton) - 1);
    max = umax(max, calcule_nb_jetons_depuis_vers(pos, 1, -1, jeton) + \
    calcule_nb_jetons_depuis_vers(pos, -1, 1, jeton) - 1);
    return max;
}
static int charger_jeu(char *nom, char *jeton, int *njoueur)
{
    /*
     * Charge une partie existante depuis le fichier spécifié.
     */
    FILE *fp;
    unsigned col;
    unsigned lgn;
    fp = fopen(nom, "r");
    if (fp == NULL)
    {
        fprintf(stderr, "Impossible d'ouvrir le fichier %s\n", nom);
        return 0;
    }
    else if (fscanf(fp, "%c%d", jeton, njoueur) != 2)
    {
        fprintf(stderr, "Impossible de récupérer le joueur et le nombre de joueurs\n");
        return 0;
    }
    for (col = 0; col < P4_COLONNES; ++col)
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
        {
            if (fscanf(fp, "|%c", &grille[col][lgn]) != 1)
            {
                fprintf(stderr, "Impossible de récupérer la grille\n");
                return 0;
            }
        }
    if (fclose(fp) != 0)
    {
        fprintf(stderr, "Impossible de fermer le fichier %s\n", nom);
        return 0;
    }
    return 1;
}
static int coup_valide(int col)
{
    /*
     * Si la colonne renseignée est inférieur ou égal à zéro
     * ou que celle-ci est supérieure à la longueur du tableau
     * ou que la colonne indiquée est saturée
     * alors le coup est invalide.
     */
    if (col <= 0 || col > P4_COLONNES || grille[col - 1][0] != ' ')
        return 0;
    return 1;
}
static int demande_action(int *coup)
{
    /*
     * Demande l'action à effectuer au joueur courant.
     * S'il entre un chiffre, c'est qu'il souhaite jouer.
     * S'il entre la lettre « Q » ou « q », c'est qu'il souhaite quitter.
     * S'il entre autre chose, une nouvelle saisie sera demandée.
     */
    char c;
    int ret = ACT_ERR;
    if (scanf("%d", coup) != 1)
    {
        if (scanf("%c", &c) != 1)
        {
            fprintf(stderr, "Erreur lors de la saisie\n");
            return ret;
        }
        switch (c)
        {
        case 'Q':
        case 'q':
            ret = ACT_QUITTER;
            break;
        case 'C':
        case 'c':
            ret = ACT_CHARGER;
            break;
        case 'S':
        case 's':
            ret = ACT_SAUVEGARDER;
            break;
        default:
            ret = ACT_NOUVELLE_SAISIE;
            break;
        }
    }
    else
        ret = ACT_JOUER;
    if (!vider_tampon(stdin))
    {
         fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
         ret = ACT_ERR;
    }
    return ret;
}
static int demande_fichier(char *nom, size_t max)
{
    /*
     * Demande et récupère un nom de fichier.
     */
    char *nl;
    printf("Veuillez entrer un nom de fichier : ");
    if (fgets(nom, max, stdin) == NULL)
    {
         fprintf(stderr, "Erreur lors de la saisie\n");
         return 0;
    }
    nl = strchr(nom, '\n');
    if (nl == NULL && !vider_tampon(stdin))
    {
       fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
       return 0;
    }
    if (nl != NULL)
        *nl = '\0';
    return 1;
}
static int demande_nb_joueur(void)
{
    /*
     * Demande et récupère le nombre de joueurs.
     */
    int njoueur = 0;
    while (1)
    {
        printf("Combien de joueurs prennent part à cette partie ? ");
        if (scanf("%d", &njoueur) != 1 && ferror(stdin))
        {
                fprintf(stderr, "Erreur lors de la saisie\n");
                return 0;
        }
        else if (!vider_tampon(stdin))
        {
            fprintf(stderr, "Erreur lors de la vidange du tampon.\n");
            return 0;
        }
        if (njoueur != 1 && njoueur != 2)
            fprintf(stderr, "Plait-il ?\n");
        else
            break;
    }
    return njoueur;
}
static int grille_complete(void)
{
    /*
     * Détermine si la grille de jeu est complète.
     */
    unsigned col;
    unsigned lgn;
    for (col = 0; col < P4_COLONNES; ++col)
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
            if (grille[col][lgn] == ' ')
                return 0;
    return 1;
}
static int ia(void)
{
    /*
     * Fonction mettant en œuvre l'IA présentée.
     * Assigne une valeur pour chaque colonne libre et retourne ensuite le numéro de
     * colonne ayant la plus haute valeur. Dans le cas où plusieurs valeurs égales sont
     * générées, un numéro de colonne est « choisi au hasard » parmi celles-ci.
     */
    unsigned meilleurs_col[P4_COLONNES];
    unsigned nb_meilleurs_col = 0;
    unsigned max = 0;
    unsigned col;
    for (col = 0; col < P4_COLONNES; ++col)
    {
        struct position pos;
        unsigned longueur;
        if (grille[col][0] != ' ')
            continue;
        calcule_position(col, &pos);
        longueur = calcule_nb_jetons_depuis(&pos, J2_JETON);
        if (longueur >= 4)
            return col;
        longueur = umax(longueur, calcule_nb_jetons_depuis(&pos, J1_JETON));
        if (longueur >= max)
        {
            if (longueur > max)
            {
                nb_meilleurs_col = 0;
                max = longueur;
            }
            meilleurs_col[nb_meilleurs_col++] = col;
        }
    }
    return meilleurs_col[nb_aleatoire_entre(0, nb_meilleurs_col - 1)];
}
static void initialise_grille(void)
{
    /*
     * Initalise les caractères de la grille.
     */
    unsigned col;
    unsigned lgn;
    for (col = 0; col < P4_COLONNES; ++col)
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
            grille[col][lgn] = ' ';
}
static unsigned umax(unsigned a, unsigned b)
{
    /*
     * Retourne le plus grand des deux arguments.
     */
    return (a > b) ? a : b;
}
double nb_aleatoire(void)
{
    /*
     * Retourne un nombre pseudo-aléatoire compris entre zéro inclus et un exclus.
     */
    return rand() / ((double)RAND_MAX + 1.);
}
int nb_aleatoire_entre(int min, int max)
{
    /*
     * Retourne un nombre pseudo-aléatoire entre `min` et `max` inclus.
     */
    return nb_aleatoire() * (max - min + 1) + min;
}
static int position_valide(struct position *pos)
{
    /*
     * Vérifie que la position fournie est bien comprise dans la grille.
     */
    int ret = 1;
    if (pos->colonne >= P4_COLONNES || pos->colonne < 0)
        ret = 0;
    else if (pos->ligne >= P4_LIGNES || pos->ligne < 0)
        ret = 0;
    return ret;
}
static int sauvegarder_jeu(char *nom, char jeton, int njoueur)
{
    /*
     * Sauvegarde la partie courant dans le fichier indiqué.
     */
    FILE *fp;
    unsigned col;
    unsigned lgn;
    fp = fopen(nom, "w");
    if (fp == NULL)
    {
        fprintf(stderr, "Impossible d'ouvrir le fichier %s\n", nom);
        return 0;
    }
    else if (fprintf(fp, "%c%d", jeton, njoueur) < 0)
    {
        fprintf(stderr, "Impossible de sauvegarder le joueur courant\n");
        return 0;
    }
    if (fputc('|', fp) == EOF)
    {
        fprintf(stderr, "Impossible de sauvegarder la grille\n");
        return 0;
    }
    for (col = 0; col < P4_COLONNES; ++col)
    {
        for (lgn = 0; lgn < P4_LIGNES; ++lgn)
        {
            if (fprintf(fp, "%c|", grille[col][lgn]) < 0)
            {
                fprintf(stderr, "Impossible de sauvegarder la grille\n");
                return 0;
            }
        }
    }
    if (fputc('\n', fp) == EOF)
    {
        fprintf(stderr, "Impossible de sauvegarder la grille\n");
        return 0;
    }
    if (fclose(fp) != 0)
    {
        fprintf(stderr, "Impossible de fermer le fichier %s\n", nom);
        return 0;
    }
    printf("La partie a bien été sauvegardée dans le fichier %s\n", nom);
    return 1;
}
static int statut_jeu(struct position *pos, char jeton)
{
    /*
     * Détermine s'il y a lieu de continuer le jeu ou s'il doit être
     * arrêté parce qu'un joueur a gagné ou que la grille est complète.
     */
    if (grille_complete())
        return STATUT_EGALITE;
    else if (calcule_nb_jetons_depuis(pos, jeton) >= 4)
        return STATUT_GAGNE;
    return STATUT_OK;
}
static int vider_tampon(FILE *fp)
{
    /*
     * Vide les données en attente de lecture du flux spécifié.
     */
    int c;
    do
        c = fgetc(fp);
    while (c != '\n' && c != EOF);
    return ferror(fp) ? 0 : 1;
}
int main(void)
{
    static char nom[MAX_NOM];
    int statut;
    char jeton = J1_JETON;
    int njoueur;
    initialise_grille();
    affiche_grille();
    njoueur = demande_nb_joueur();
    if (!njoueur)
        return EXIT_FAILURE;
    while (1)
    {
        struct position pos;
        int action;
        int coup;
        if (njoueur == 1 && jeton == J2_JETON)
        {
            coup = ia();
            printf("Joueur 2 : %d\n", coup + 1);
            calcule_position(coup, &pos);
        }
        else
        {
            printf("Joueur %d : ", (jeton == J1_JETON) ? 1 : 2);
            action = demande_action(&coup);
            if (action == ACT_ERR)
                return EXIT_FAILURE;
            else if (action == ACT_QUITTER)
                return 0;
            else if (action == ACT_SAUVEGARDER)
            {
                if (!demande_fichier(nom, sizeof nom) || !sauvegarder_jeu(nom, jeton, njoueur))
                    return EXIT_FAILURE;
                else
                    return 0;
            }
            else if (action == ACT_CHARGER)
            {
                if (!demande_fichier(nom, sizeof nom) || !charger_jeu(nom, &jeton, &njoueur))
                    return EXIT_FAILURE;
                else
                {
                    affiche_grille();
                    continue;
                }
            }
            else if (action == ACT_NOUVELLE_SAISIE || !coup_valide(coup))
            {
                fprintf(stderr, "Vous ne pouvez pas jouer à cet endroit\n");
                continue;
            }
            calcule_position(coup - 1, &pos);
        }
        grille[pos.colonne][pos.ligne] = jeton;
        affiche_grille();
        statut = statut_jeu(&pos, jeton);
        if (statut != STATUT_OK)
            break;
        jeton = (jeton == J1_JETON) ? J2_JETON : J1_JETON;
    }
    if (statut == STATUT_GAGNE)
        printf("Le joueur %d a gagné\n", (jeton == J1_JETON) ? 1 : 2);
    else if (statut == STATUT_EGALITE)
        printf("Égalité\n");
    return 0;
}

À présent, durant la partie, un joueur peut entrer la lettre « s » (en minuscule ou en majuscule) afin d’effectuer une sauvegarde de la partie courante au sein d’un fichier qu’il doit spécifier. Les données sont sauvegardées sous forme de texte avec, dans l’ordre : le joueur courant, le nombre de joueurs et le contenu de la grille. Une fois la sauvegarde effectuée, le jeu est stoppé.

Il peut également entrer la lettre « c » (de nouveau en minuscule ou en majuscule) pour charger une partie précédemment sauvegardée.

Dans le chapitre suivant, nous verrons comment rendre nos programmes plus robustes en approfondissant la gestion d’erreurs.


La gestion d'erreurs (2) - Le langage C • Tutoriels • Zeste de Savoir

La gestion d'erreurs 2

Gestion de ressources

Allocation de ressources

Dans cette deuxième partie, nous avons découvert l’allocation dynamique de mémoire et la gestion de fichiers. Ces deux sujets ont un point en commun : il est nécessaire de passer par une fonction d’allocation et par une fonction de libération lors de leur utilisation. Il s’agit d’un processus commun en programmation appelé la gestion de ressources. Or, celle-ci pose un problème particulier dans le cas de la gestion d’erreurs. En effet, regardez de plus près ce code simplifié permettant l’allocation dynamique d’un tableau multidimensionnel de trois fois trois int .

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int **p;
    unsigned i;
    p = malloc(3 * sizeof(int *));
    if (p == NULL)
    {
        fprintf(stderr, "Échec de l'allocation\n");
        return EXIT_FAILURE;
    }
    for (i = 0; i < 3; ++i)
    {
        p[i] = malloc(3 * sizeof(int));
        if (p[i] == NULL)
        {
            fprintf(stderr, "Échec de l'allocation\n");
            return EXIT_FAILURE;
        }
    }
    /* ... */
    return 0;
}

Vous ne voyez rien d’anormal dans le cas de la seconde boucle ? Celle-ci quitte le programme en cas d’échec de la fonction malloc() . Oui, mais nous avons déjà fait appel à la fonction malloc() auparavant, ce qui signifie que si nous quittons le programme à ce stade, nous le ferons sans avoir libéré certaines ressources (ici de la mémoire).

Seulement voilà, comment faire cela sans rendre le programme bien plus compliqué et bien moins lisible ? C’est ici que l’utilisation de l’instruction goto devient pertinente et d’une aide précieuse. La technique consiste à placer la libération de ressources en fin de fonction et de se rendre au bon endroit de cette zone de libération à l’aide de l’instruction goto . Autrement dit, voici ce que cela donne avec le code précédent.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int **p;
    unsigned i;
    int status = EXIT_FAILURE;
    p = malloc(3 * sizeof(int *));
    if (p == NULL)
    {
        fprintf(stderr, "Échec de l'allocation\n");
        goto fin;
    }
    for (i = 0; i < 3; ++i)
    {
        p[i] = malloc(3 * sizeof(int));
        if (p[i] == NULL)
        {
            fprintf(stderr, "Échec de l'allocation\n");
            goto liberer_p;
        }
    }
    status = 0;
    /* Zone de libération */
liberer_p:
    while (i > 0)
    {
        --i;
        free(p[i]);
    }
    free(p);
fin:
    return status;
}

Comme vous le voyez, nous avons placé deux étiquettes : une référençant l’instruction return et une autre désignant la première instruction de la zone de libération. Ainsi, nous quittons la fonction en cas d’échec de la première allocation, mais nous libérons les ressources auparavant dans le cas des allocations suivantes. Notez que nous avons ajouté une variable status afin de pouvoir retourner la bonne valeur suivant qu’une erreur est survenue ou non.

Utilisation de ressources

Si le problème se pose dans le cas de l’allocation de ressources, il se pose également dans le cas de leur utilisation.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp;
    fp = fopen("texte.txt", "w");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert\n");
        return EXIT_FAILURE;
    }
    if (fputs("Au revoir !\n", fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de l'écriture d'une ligne\n");
        return EXIT_FAILURE;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        return EXIT_FAILURE;
    }
    return 0;
}

Dans le code ci-dessus, l’exécution du programme est stoppée en cas d’erreur de la fonction fputs() . Cependant, l’arrêt s’effectue alors qu’une ressource n’a pas été libérée (en l’occurrence le flux fp ). Aussi, il est nécessaire d’appliquer la solution présentée juste avant pour rendre ce code correct.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp;
    int status = EXIT_FAILURE;
    fp = fopen("texte.txt", "w");
    if (fp == NULL)
    {
        fprintf(stderr, "Le fichier texte.txt n'a pas pu être ouvert\n");
        goto fin;
    }
    if (fputs("Au revoir !\n", fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de l'écriture d'une ligne\n");
        goto fermer_flux;
    }
    if (fclose(fp) == EOF)
    {
        fprintf(stderr, "Erreur lors de la fermeture du flux\n");
        goto fin;
    }
    status = 0;
fermer_flux:
    fclose(fp);
fin:
    return status;
}

Nous attirons votre attention sur deux choses :

  • En cas d’échec de la fonction fclose() , le programme est arrêté immédiatement. Étant donné que c’est la fonction fclose() qui pose problème, il n’y a pas lieu de la rappeler (notez que cela n’est toutefois pas une erreur de l’appeler une seconde fois).
  • Le retour de la fonction fclose() n’est pas vérifié en cas d’échec de la fonction fputs() étant donné que nous sommes déjà dans un cas d’erreur.

Fin d'un programme

Vous le savez, l’exécution de votre programme se termine lorsque celui-ci quitte la fonction main() . Toutefois, que se passe-t-il exactement lorsque celui-ci s’arrête ? En fait, un petit bout de code appelé épilogue est exécuté afin de réaliser quelques tâches. Parmi celles-ci figure la vidange et la fermeture de tous les flux encore ouverts. Une fois celui-ci exécuté, la main est rendue au système d’exploitation qui, le plus souvent, se chargera de libérer toutes les ressources qui étaient encore allouées.

Mais ?! Vous venez de nous dire qu’il était nécessaire de libérer les ressources avant l’arrêt du programme. Du coup, pourquoi s’amuser à appeler les fonctions fclose() ou free() alors que l’épilogue ou le système d’exploitation s’en charge ?

Pour quatre raisons principales :

  1. Afin de libérer des ressources pour les autres programmes. En effet, si vous ne libérerez les ressources allouées qu’à la fin de votre programme alors que celui-ci n’en a plus besoin, vous gaspillez des ressources qui pourraient être utilisées par d’autres programmes.
  2. Pour être à même de détecter des erreurs, de prévenir l’utilisateur en conséquence et d’éventuellement lui proposer des solutions.
  3. En vue de rendre votre code plus facilement modifiable par après et d’éviter les oublis.
  4. Parce qu’il n’est pas garanti que les ressources seront effectivement libérées par le système d’exploitaion.

Aussi, de manière générale :

  • considérez que l’objectif de l’épilogue est de fermer uniquement les flux stdin , stdout et stderr ;
  • ne comptez pas sur votre système d’exploitation pour la libération de quelques ressources que ce soit (et notamment la mémoire) ;
  • libérez vos ressources le plus tôt possible, autrement dit dès que votre programme n’en a plus l’utilité.

Terminaison normale

Le passage par l’épilogue est appelée la terminaison normale du programme. Il est possible de s’y rendre directement en appelant la fonction exit() qui est déclarée dans l’en-tête <stdlib.h> .

void exit(int status);

Appeler cette fonction revient au même que de quitter la fonction main() à l’aide de l’instruction return . L’argument attendu est une expression entière identique à celle fournie comme opérande de l’instruction return . Ainsi, il vous est possible de mettre fin directement à l’exécution de votre programme sans retourner jusqu’à la fonction main() .

#include <stdio.h>
#include <stdlib.h>
void fin(void)
{
    printf("C'est la fin du programme\n");
    exit(0);
}
int main(void)
{
    fin();
    printf("Retour à la fonction main\n");
    return 0;
}
Résultat
C'est la fin du programme

Comme vous le voyez l’exécution du programme se termine une fois que la fonction exit() est appelée. La fin de la fonction main() n’est donc jamais exécutée.

Terminaison anormale

Il est possible de terminer l’exécution d’un programme sans passer par l’épilogue à l’aide de la fonction abort() (définie également dans l’en-tête <stdlib.h> ). Dans un tel cas, il s’agit d’une terminaison anormale du programme.

void abort(void);

Une terminaison anormale signifie qu’une condition non prévue par le programmeur est survenue. Elle est donc à distinguer d’une terminaison normale survenue suite à une erreur qui, elle, était prévue (comme une erreur lors d’une saisie de l’utilisateur). De manière générale, la terminaison anormale est utilisée lors de la phase de développement d’un logiciel afin de faciliter la détection d’erreurs de programmation, celle-ci entraînant le plus souvent la production d’une image mémoire ( core dump en anglais) qui pourra être analysée à l’aide d’un débogueur ( debugger en anglais).

Une image mémoire est en fait un fichier contenant l’état des registres et de la mémoire d’un programme lors de la survenance d’un problème..

Les assertions

La macrofonction assert

Une des utilisations majeure de la fonction abort() est réalisée via la macrofonction assert() (définie dans l’en-tête <assert.h> ). Cette dernière est utilisée pour placer des tests à certains points d’un programme. Dans le cas où un de ces tests s’avère faux, un message d’erreur est affiché (ce dernier comprend la condition dont l’évaluation est fausse, le nom du fichier et la ligne courante) après quoi la fonction abort() est appelée afin de produire une image mémoire.

Cette technique est très utile pour détecter rapidement des erreurs au sein d’un programme lors de sa phase de développement. Généralement, les assertions sont placées en début de fonction afin de vérifier un certain nombres de conditions. Par exemple, si nous reprenons la fonction long_colonne() du TP sur le Puissance 4.

static unsigned long_colonne(unsigned joueur, unsigned col, unsigned ligne, unsigned char (*grille)[6])
{
    unsigned i;
    unsigned n = 1;
    for (i = ligne + 1; i < 6; ++i)
    {
        if (grille[col][i] == joueur)
            ++n;
        else
            break;
    }
    return n;
}

Nous pourrions ajouter quatre assertions vérifiant si :

  • le numéro du joueur est un ou deux ;
  • le numéro de la colonne est compris entre zéro et six inclus ;
  • le numéro de la ligne est compris entre zéro et cinq ;
  • le pointeur grille n’est pas nul.

Ce qui nous donne le code suivant.

static unsigned long_colonne(unsigned joueur, unsigned col, unsigned ligne, unsigned char (*grille)[6])
{
    unsigned i;
    unsigned n = 1;
    assert(joueur == 1 || joueur == 2);
    assert(col < 7);
    assert(ligne < 6);
    assert(grille != NULL);
    for (i = ligne + 1; i < 6; ++i)
    {
        if (grille[col][i] == joueur)
            ++n;
        else
            break;
    }
    return n;
}

Ainsi, si nous ne respectons pas une de ces conditions pour une raison ou pour une autre, l’exécution du programme s’arrêtera et nous aurons droit à un message du type (dans le cas où la première assertion échoue).

a.out: main.c:71: long_colonne: Assertion `joueur == 1 || joueur == 2' failed.
Aborted

Comme vous le voyez, le nom du programme est indiqué, suivi du nom du fichier, du numéro de ligne, du nom de la fonction et de la condition qui a échoué. Intéressant, non ? :)

Suppression des assertions

Une fois votre programme développé et dûment testé, les assertions ne vous sont plus vraiment utiles étant donné que celui-ci fonctionne. Les conserver alourdirait l’exécution de votre programme en ajoutant des vérifications. Toutefois, heureusement, il est possible de supprimer ces assertions sans modifier votre code en ajoutant simplement l’option -DNDEBUG lors de la compilation.

$ zcc -DNDEBUG main.c

Les fonctions strerror et perror

Vous le savez, certaines (beaucoup) de fonctions standards peuvent échouer. Toutefois, en plus de signaler à l’utilisateur qu’une de ces fonctions a échoué, cela serait bien de lui spécifier pourquoi . C’est ici qu’entre en jeu les fonctions sterror() et perror() .

char *strerror(int num);

La fonction strerror() (déclarée dans l’en-tête <string.h> ) retourne une chaîne de caractères correspondant à la valeur entière fournie en argument. Cette valeur sera en fait toujours celle de la variable errno . Ainsi, il vous est possible d’obtenir plus de détails quand une fonction standard rencontre un problème. L’exemple suivant illustre l’utilisation de cette fonction en faisant appel à la fonction fopen() afin d’ouvrir un fichier qui n’existe pas (ce qui provoque une erreur).

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void)
{
    FILE *fp;
    fp = fopen("nawak.txt", "r");
    if (fp == NULL)
    {
        fprintf(stderr, "fopen: %s\n", strerror(errno));
        return EXIT_FAILURE;
    }
    fclose(fp);
    return 0;
}
Résultat
fopen: No such file or directory

Comme vous le voyez, nous avons obtenu des informations supplémentaires : la fonction a échoué parce que le fichier « nawak.txt » n’existe pas.

Notez que les messages d’erreur retournés par la fonction strerror() sont le plus souvent en anglais.

void perror(char *message);

La fonction perror() (déclarée dans l’en-tête <stdio.h> ) écrit sur le flux d’erreur standard ( stderr , donc) la chaîne de caractères fournie en argument, suivie du caractère : , d’un espace et du retour de la fonction strerror() avec comme argument la valeur de la variable errno . Autrement dit, cette fonction revient au même que l’appel suivant.

fprintf(stderr, "message: %s\n", strerror(errno));

L’exemple précédent peut donc également s’écrire comme suit.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    FILE *fp;
    fp = fopen("nawak.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return EXIT_FAILURE;
    }
    fclose(fp);
    return 0;
}
Résultat
fopen: No such file or directory

Voici qui clôture la deuxième partie de ce cours qui aura été riche en nouvelles notions. N’hésitez pas à reprendre certains passages avant de commencer la troisième partie qui vous fera plonger sous le capot.

En résumé

  • La gestion des ressources implique une gestion des erreurs ;
  • Le mieux est de libérer les ressources inutilisées le plus tôt possible, pour ne pas les monopoliser pour rien ;
  • Les ressources doivent être libérées correctement et non laissées aux soins du système d’exploitation ;
  • La fonction abort() termine brutalement le programme en cas d’erreur ;
  • Les assertions placent des tests à certains endroits du code et sont utiles pour détecter rapidement des erreurs de la phase de développement d’un programme ;
  • Elles sont désactivées en utilisant l’option de compilation -DNDEBUG ;
  • La fonction strerror() retourne une chaîne de caractère qui correspond au code d’erreur que contient errno ;
  • La fonction perror() fait la même chose, en affichant en plus, sur stdout , un message passé en argument suivi de : et d’un espace.


La représentation des types - Le langage C • Tutoriels • Zeste de Savoir

La représentation des types

La représentation des entiers

Les entiers non signés

La représentation des entiers non signés étant la plus simple, nous allons commencer par celle-ci. :)

Dans un entier non signé, les différents bits correspondent à une puissance de deux. Plus précisément, le premier correspond à 20, le second à 21, le troisième à 22 et ainsi de suite jusqu’au dernier bit composant le type utilisé. Pour calculer la valeur d’un entier non signé, il suffit donc d’additionner les puissances de deux correspondant aux bits à 1.

Pour illustrer notre propos, voici un tableau comprenant la représentation de plusieurs nombres au sein d’un objet de type unsigned char (ici sur un octet).

Nombre | Représentation
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1
 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 20 = 1
3 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1
 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 20 + 21 = 3
42 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 0
 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 21 + 23 + 25 = 42
255 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 = 255

Les entiers signés

Les choses se compliquent un peu avec les entiers signés. En effet, il nous faut représenter une information supplémentaire : le signe de la valeur.

La représentation en signe et magnitude

La première solution qui vous est peut-être venue à l’esprit serait de réserver un bit , par exemple le dernier, pour représenter le signe. Ainsi, la représentation est identique à celle des entiers non signés si ce n’est que le dernier bit est réservé pour le signe (ce qui diminue donc en conséquence la valeur maximale représentable). Cette méthode est appelée représentation en signe et magnitude.

Nombre | Représentation en signe et magnitude
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1
 | + | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 20 = 1
-1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1
 | - | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | -20 = -1

Cependant, si la représentation en signe et magnitude paraît simple, elle n’est en vérité pas facile à mettre en œuvre au sein d’un processeur car elle implique plusieurs vérifications (notamment au niveau du signe) qui se traduisent par des circuits supplémentaires et un surcoût en calcul. De plus, elle laisse deux représentations possibles pour le zéro ( 0000 0000 et 1000 0000 ), ce qui est gênant pour l’évaluation des conditions.

La représentation en complément à un

Dès lors, comment pourrions-nous faire pour simplifier nos calculs en évitant des vérifications liées au signe ? Eh bien, sachant que le maximum représentable dans notre exemple est 255 (soit 1111 1111 ), il nous est possible de représenter chaque nombre négatif comme la différence entre le maximum et sa valeur absolue. Par exemple -1 sera représenté par 255 - 1 , soit 254 ( 1111 1110 ). Cette représentation est appelée représentation en complément à un.

Ainsi, si nous additionnons 1 et -1 , nous n’avons pas de vérifications à faire et obtenons le maximum, 255 . Toutefois, ceci implique, comme pour la représentation en signe et magnitude, qu’il existe deux représentations pour le zéro : 0000 0000 et 1111 1111 .

Nombre | Représentation en complément à un
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1
 | + | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 20 = 1
-1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 0
 | -/27 | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | -(255 - 27 - 26 - 25 - 24 - 23 - 22 - 21) = -1

Notez qu’en fait, cette représentation revient à inverser tous les bits d’un nombre positif pour obtenir son équivalent négatif. Par exemple, si nous inversons 1 ( 0000 0001 ), nous obtenons bien -1 ( 1111 1110 ) comme ci-dessus.

Par ailleurs, il subsiste un second problème : dans le cas où deux nombres négatifs sont additionnés, le résultat obtenu n’est pas valide. En effet, -1 + -1 nous donne 1111 1110 + 1111 1110 soit 1 1111 1100 ; autrement dit, comme nous travaillons sur huit bits pour nos exemples, -3 … Mince !

Pour résoudre ce problème, il nous faut reporter la dernière retenue (soit ici le dernier bit que nous avons ignoré) au début (ce qui revient à ajouter un) ce qui nous permet d’obtenir 1111 1101 , soit -2 .

La représentation en complément à deux

Ainsi est apparue une troisième représentation : celle en complément à deux. Celle-ci conserve les qualités de la représentation en complément à un, mais lui corrige certains défauts. En fait, elle représente les nombres négatifs de la même manière que la représentation en complément à un, si ce n’est que la soustraction s’effectue entre le maximum augmenté de un (soit 256 dans notre cas) et non le maximum.

Ainsi, par exemple, la représentation en complément à deux de -1 est 256 - 1 , soit 255 ( 1111 1111 ).

Nombre | Représentation en complément à deux
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1
 | + | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 20 = 1
-1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
 | -/27 | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | -(256 - 27 - 26 - 25 - 24 - 23 - 22 - 21 - 20) = -1

Remarquez que cette représentation revient finalement à inverser tous les bits d’un nombre positif et à augmenter le résultat de un en vue d’obtenir son équivalent négatif. Par exemple, si nous inversons les bits du nombre 1 nous obtenons 1111 1110 et si nous lui ajoutons un, nous avons bien 1111 1111 .

Cette fois ci, l’objectif recherché est atteint ! :)

En effet, si nous additionnons 1 et -1 (soit 0000 0001 et 1111 1111 ) et que nous ignorons la retenue, nous obtenons bien zéro. De plus, il n’y a plus de cas particulier de retenue à déplacer comme en complément à un, puisque, par exemple, la somme -1 + -2 , soit 1111 1111 + 1111 1110 donne 1 1111 1101 , autrement dit -3 sans la retenue . Enfin, nous n’avons plus qu’une seule représentation pour le zéro.

Point de norme

La norme du langage C ne précise pas quelle représentation doit être utilisée pour les entiers signés 1. Elle impose uniquement qu’il s’agisse d’une de ces trois. Cependant, dans les faits, il s’agit presque toujours de la représentation en complément à deux.

Point de norme

Notez que chacune de ces représentations dispose toutefois d’une suite de bits qui est susceptible de ne pas représenter une valeur et de produire une erreur en cas d’utilisation 2.

  • Pour les représentations en signe et magnitude et en complément à deux, il s’agit de la suite où tous les bits sont à zéro et le bit de signe à un : 1000 0000 ;
  • Pour la représentation en complément à un, il s’agit de la suite où tous les bits sont à un, y compris le bit de signe : 1111 1111 .

Dans le cas des représentations en signe et magnitude et en complément à un, il s’agit des représentations possibles pour le « zéro négatif ». Pour la représentation en complément à deux, cette suite est le plus souvent utilisée pour représenter un nombre négatif supplémentaire (dans le cas de 1000 0000 , il s’agira de -128 ). Toutefois, même si ces représentations peuvent être utilisées pour représenter une valeur valide, ce n’est pas forcément le cas.

Néanmoins, rassurez-vous, ces valeurs ne peuvent être produites dans le cas de calculs ordinaires, sauf survenance d’une condition exceptionnelle comme un dépassement de capacité (nous en parlerons bientôt). Vous n’avez donc pas de vérifications supplémentaires à effectuer à leur égard. Évitez simplement d’en produire une délibérément, par exemple en l’affectant directement à une variable.

Les bits de bourrages

Il est important de préciser que tous les bits composant un type entier ne sont pas forcément utilisés pour représenter la valeur qui y est stockée. Nous l’avons vu avec le bit de signe, qui ne correspond pas à une valeur. La norme prévoit également qu’il peut exister des bits de bourrages, et ce aussi bien pour les entiers signés que pour les entiers non signés à l’exception du type char . Ceux-ci peuvent par exemple être employés pour maintenir d’autres informations (par exemple : le type de la donnée stockée, ou encore un bit de parité pour vérifier l’intégrité de celle-ci).

Par conséquent, il n’y a pas forcément une corrélation parfaite entre le nombres de bits composant un type entier et la valeur maximale qui peut y être stockée.

Sachez toutefois que les bits de bourrages au sein des types entiers sont assez rares, les architectures les plus courantes n’en emploient pas.

Les entiers de taille fixe

Face à ces incertitudes quant à la représentation des entiers, la norme C99 a introduit de nouveaux types entiers : les entiers de taille fixe. À la différence des autres types entiers, leur représentation est plus stricte :

  1. Ces types ne comprennent pas de bits de bourrage ;
  2. Comme leur nom l’indique, leur taille est un nombre de bits fixe ;
  3. La représentation en complément à deux est utilisée pour les nombres négatifs.

Ces types sont définis dans l’en-tête <stdint.h> et sont les suivants.

Type | Minimum | Maximum
int8_t | -128 | 127
 | INT8_MIN | INT8_MAX
uint8_t | 0 | 255
 |  | UINT8_MAX
int16_t | -32.768 | 32.767
 | INT16_MIN | INT16_MAX
uint16_t | 0 | 65.535
 |  | UINT16_MAX
int32_t | -2.147.483.648 | 2.147.483.647
 | INT32_MIN | INT32_MAX
uint32_t | 0 | 4.294.967.295
 |  | UINT32_MAX
int64_t | -9.223.372.036.854.775.808 | 9.223.372.036.854.775.807
 | INT64_MIN | INT64_MAX
uint64_t | 0 | 18.446.744.073.709.551.615
 |  | UINT64_MAX
Notez que les limites sont ici exactes, le nombre de bits étant fixé et l’absence de bits de bourrage garantie.

Pour le reste, ces types s’utilisent comme les autres types entiers, si ce n’est qu’ils n’ont pas d’indicateur de conversion spécifique. À la place, il est nécessaire de recourir à des macroconstantes qui sont remplacées par une chaîne de caractères comportant le bon indicateur de conversion. Ces dernières sont définies dans l’en-tête <inttypes.h> et sont les suivantes.

Type | Macroconstantes
int8_t | PRId8 (ou PRIi8) et SCNd8 (ou SCNi8)
uint8_t | PRIu8 (ou PRIo8, PRIx8, PRIX8) et SCNu8 (ou SCNo8, SCNx8)
int16_t | PRId16 (ou PRIi16) et SCNd16 (ou SCNi16)
uint16_t | PRIu16 (ou PRIo16, PRIx16, PRIX16) et SCNu16 (ou SCNo16, SCNx16)
int32_t | PRId32 (ou PRIi32) et SCNd32 (ou SCNi32)
uint32_t | PRIu32 (ou PRIo32, PRIx32, PRIX32) et SCNu32 (ou SCNo32, SCNx32)
int64_t | PRId64 (ou PRIi64) et SCNd64 (ou SCNi64)
uint64_t | PRIu64 (ou PRIo64, PRIx64, PRIX64) et SCNu64 (ou SCNo64, SCNx64)
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
int
main(void)
{
    int16_t n = INT16_MIN;
    uint64_t m = UINT64_MAX;
    printf("n = %" PRId16 ", m = %" PRIu64 "\n", n, m);
    return 0;
}
Résultat
n = -32768, m = 18446744073709551615

Le support de ces types est facultatif , il est donc possible qu’ils ne soient pas disponibles, bien que cela soit assez peu probable.

  1. ISO/IEC 9899:201x, doc. N1570, § 6.2.6.2, Integer types, p. 45.

    ↩

  2. ISO/IEC 9899:201x, doc. N1570, § 6.2.6.1, General, al. 5, p. 44.

    ↩

La représentations des flottants

La représentation des types flottants amène deux difficultés supplémentaires :

  • la gestion de nombres réels, c’est-à-dire potentiellement composés d’une partie entière et d’une suite de décimales ;
  • la possibilité de stocker des nombres de différents ordres de grandeur, entre 10-37 et 1037.

Première approche

Avec des entiers

Une première solution consisterait à garantir une précision à 10-37 en utilisant deux nombres entiers : un, signé, pour la partie entière et un, non signé, pour stocker la suite de décimales.

Cependant, si cette approche a le mérite d’être simple, elle a le désavantage d’utiliser beaucoup de mémoire. En effet, pour stocker un entier de l’ordre de 1037, il serait nécessaire d’utiliser un peu moins de 128 bits , soit 16 octets (et donc environ 32 octets pour la représentation globale). Un tel coût est inconcevable, même à l’époque actuelle.

Une autre limite provient de la difficulté d’effectuer des calculs sur les nombres flottants avec une telle représentation : il faudrait tenir compte du passage des décimales vers la partie entière et inversement.

Avec des puissances de deux négatives

Une autre représentation possible consiste à attribuer des puissances de deux négatives à une partie des bits . Les calculs sur les nombres flottants obtenus de cette manière sont similaires à ceux sur les nombres entiers. Le tableau ci-dessous illustre ce concept en divisant un octet en deux : quatre bits pour la partie entière et quatre bits pour la partie fractionnaire.

Nombre | Représentation
1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0
 | 23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4
 | 20 = 1
1,5 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0
 | 23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4
 | 2-1 + 20 = 1,5
0,875 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0
 | 23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4
 | 2-3 + 2-2 + 2-1 = 0,875

Toutefois, notre problème de capacité persiste : il nous faudra toujours une grande quantité de mémoire pour pouvoir stocker des nombres d’ordre de grandeur aussi différents.

Une histoire de virgule flottante

Par conséquent, la solution qui a été retenue historiquement consiste à réserver quelques bits qui contiendront la valeur d’un exposant. Celui-ci sera ensuite utilisé pour déterminer à quelles puissances de deux correspondent les bits restants. Ainsi, la virgule séparant la partie entière de sa suite décimales est dite « flottante », car sa position est ajustée par l’exposant. Toutefois, comme nous allons le voir, ce gain en mémoire ne se réalise pas sans sacrifice : la précision des calculs va en pâtir.

Le tableau suivant utilise deux octets : un pour stocker un exposant sur sept bits avec un bit de signe ; un autre pour stocker la valeur du nombre. L’exposant est lui aussi signé et est représenté en signe et magnitude (par souci de facilité). Par ailleurs, cet exposant est attribué au premier bit du deuxième octet ; les autres correspondent à une puissance de deux chaque fois inférieure d’une unité.

Nombre | Signe | Exposant |  |  |  |  |  |  | Bits du nombre
1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0
 | + | + | 25 | 24 | 23 | 22 | 21 | 20 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | 2-5 | 2-6 | 2-7
 | + | 0 |  |  |  |  |  |  | 20 = 1
-1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0
 | - | + | 25 | 24 | 23 | 22 | 21 | 20 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | 2-5 | 2-6 | 2-7
 | - | 0 |  |  |  |  |  |  | -20 = -1
0,5 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0
 | + | + | 25 | 24 | 23 | 22 | 21 | 20 | 20 | 2-1 | 2-2 | 2-3 | 2-4 | 2-5 | 2-6 | 2-7
 | + | 0 |  |  |  |  |  |  | 2-1 = 0,5
10 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 0
 | + | + | 25 | 24 | 23 | 22 | 21 | 20 | 23 | 22 | 21 | 20 | 2-1 | 2-2 | 2-3 | 2-4
 | + | 21 + 20 = 3 |  |  |  |  |  |  | 23 + 21 = 10
0,00001 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1
 | + | - | 25 | 24 | 23 | 22 | 21 | 20 | 2-17 | 2-18 | 2-19 | 2-20 | 2-21 | 2-22 | 2-23 | 2-24
 | + | -21 + -24 = -17 |  |  |  |  |  |  | 2-24 + 2-23 + 2-22 + 2-20 + 2-19 + 2-17 = ~0,00001
32769 | 0 | 0 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0
 | + | + | 25 | 24 | 23 | 22 | 21 | 20 | 215 | 214 | 213 | 212 | 211 | 210 | 29 | 28
 | + | 20 + 21 + 22 + 23 = 15 |  |  |  |  |  |  | 215 = 32768

Les quatre premiers exemples n’amènent normalement pas de commentaires particuliers ; néanmoins, les deux derniers illustrent deux problèmes posés par les nombres flottants.

Les approximations

Les nombres réels mettent en évidence la difficulté (voir l’impossibilité) de représenter une suite de décimales à l’aide d’une somme de puissances de deux. En effet, le plus souvent, les valeurs ne peuvent être qu’approchées et obtenues suite à des arrondis. Dans le cas de 0,00001, la somme 2-24 + 2-23 + 2-22 + 2-20 + 2-19 + 2-17 donne en vérité 0,000010907649993896484375 qui, une fois arrondie, donnera bien 0,00001.

Les pertes de précision

Dans le cas où la valeur à représenter ne peut l’être entièrement avec le nombre de bits disponible (autrement dit, le nombre de puissances de deux disponibles), la partie la moins significative sera abandonnée et, corrélativement, de la précision.

Ainsi, dans notre exemple, si nous souhaitons représenter le nombre 32769, nous avons besoin d’un exposant de 15 afin d’obtenir 32768. Toutefois, vu que nous ne possédons que de 8 bits pour représenter notre nombre, il nous est impossible d’attribuer l’exposant 0 à un des bits . Cette information est donc perdue et seule la valeur la plus significative (ici 32768) est conservée.

Formalisation

L’exemple simplifié que nous vous avons montré illustre dans les grandes lignes la représentation des nombres flottants. Cependant, vous vous en doutez, la réalité est un peu plus complexe. De manière plus générale, un nombre flottant est représenté à l’aide : d’un signe, d’un exposant et d’une mantisse qui est en fait la suite de puissances qui sera additionnée.

Par ailleurs, nous avons utilisé une suite de puissance de deux, mais il est parfaitement possible d’utiliser une autre base. Beaucoup de calculatrices utilisent par exemple des suites de puissances de dix et non de deux.

IEEE 754

À l’heure actuelle, les nombres flottants sont presque toujours représentés suivant la norme IEEE 754 qui utilise une représentation en base deux. Si vous souhaitez en apprendre davantage, nous vous invitons à lire le tutoriel Introduction à l’arithmétique flottante d’Aabu

La représentation des pointeurs

Cet extrait sera relativement court : la représentation des pointeurs est indéterminée. Sur la plupart des architectures, il s’agit en vérité d’entiers non signés, mais il n’y a absolument aucune garantie à ce sujet.

Ordre des multiplets et des bits

Jusqu’à présent, nous vous avons montré les représentations binaires en ordonnant les bits et les multiplets par puissance de deux croissantes. Cependant, s’il s’agit d’une représentation possible, elle n’est pas la seule. En vérité, l’ordre des multiplets et des bits peut varier d’une machine à l’autre.

Ordre des multiplets

Dans le cas où un type est composé de plus d’un multiplet (ce qui, à l’exception du type char , est pour ainsi dire toujours le cas), ceux-ci peuvent être agencés de différentes manières. L’ordre des multiplets d’une machine est appelé son boutisme ( endianness en anglais).

Il en existe principalement deux : le gros-boutisme et le petit-boutisme.

Le gros-boutisme

Sur une architecture gros-boutiste, les multiplets sont ordonnés par poids décroissant.

Le poids d’un bit se détermine suivant la puissance de deux qu’il représente : plus elle est élevée, plus le bit a un poids important. Le bit représentant la puissance de deux la plus basse est appelé de bit de poids faible et celui correspondant à la puissance la plus grande est nommé bit de poids fort. Pour les multiplets, le raisonnement est identique : son poids est déterminé par celui des bits le composant.

Autrement dit, une machine gros-boutiste place les multiplets comprenant les puissances de deux les plus élevées en premier (nos exemples précédents utilisaient donc cette représentation).

Le petit-boutisme

Une architecture petit-boutiste fonctionne de manière inverse : les multiplets sont ordonnés par poids croissant.

Ainsi, si nous souhaitons stocker le nombre 1 dans une variable de type unsigned short (qui sera, pour notre exemple, d’une taille de deux octets), nous aurons les deux résultats suivants, selon le boutisme de la machine.

Nombre | Représentation gros-boutiste
1 | Octet de poids fort |  |  |  |  |  |  |  | Octet de poids faible
 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1
 | 215 | 214 | 213 | 212 | 211 | 210 | 29 | 28 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20
 | 20 = 1
Nombre | Représentation petit-boutiste
1 | Octet de poids faible |  |  |  |  |  |  |  | Octet de poids fort
 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
 | 27 | 26 | 25 | 24 | 23 | 22 | 21 | 20 | 215 | 214 | 213 | 212 | 211 | 210 | 29 | 28
 | 20 = 1

Le boutisme est relativement transparent du point de vue du programmeur, puisqu’il s’agit d’une propriété de la mémoire. En pratique, de tels problèmes ne se posent que lorsqu’on accède à la mémoire à travers des types différents de celui de l’objet stocké initialement (par exemple via un pointeur sur char ). En particulier, la communication entre plusieurs machines doit les prendre en compte.

Ordre des bits

Cependant, ce serait trop simple si le problème en restait là… :-°
Malheureusement, l’ordre des bits varie également d’une machine à l’autre et ce, indépendamment de l’ordre des multiplets . Comme pour les multiplets, il existe différente possibilités, mais les plus courantes sont le gros-boutisme et le petit-boutisme. Ainsi, il est par exemple possible que la représentation des multiplets soit gros-boutiste, mais que celle des bits soit petit-boutiste (et inversement).

Nombre | Représentation gros-boutiste pour les multiplets et petit-boutiste pour les bits
1 | Octet de poids fort |  |  |  |  |  |  |  | Octet de poids faible
 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0
 | 28 | 29 | 210 | 211 | 212 | 213 | 214 | 215 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27
 | 20 = 1
Nombre | Représentation petit-boutiste pour les multiplets et petit-boutiste pour les bits
1 | Octet de poids faible |  |  |  |  |  |  |  | Octet de poids fort
 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0
 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 210 | 211 | 212 | 213 | 214 | 215
 | 20 = 1

Notez toutefois que, le plus souvent, une représentation petit-boutiste ou gros-boutiste s’applique aussi bien aux octets qu’aux bits .
Par ailleurs, sachez qu’à quelques exceptions près, l’ordre du stockage des bits n’apparaît pas en C. En particulier, les opérateurs de manipulation de bits , vus au chapitre suivant, n’en dépendent pas.

Applications

Connaître le boutisme d’une machine

Le plus souvent, il est possible de connaître le boutisme employé pour les multiplets à l’aide du code suivant. Ce dernier stocke la valeur 1 dans une variable de type unsigned short , et accède à son premier octet à l’aide d’un pointeur sur unsigned char . Si celui-ci est nul, c’est que la machine est gros-boutiste, sinon, c’est qu’elle est petit-boutiste.

#include <stdio.h>
int main(void)
{
    unsigned short n = 1;
    unsigned char *p = (unsigned char *)&n;
    if (*p != 0)
        printf("Je suis petit-boutiste.\n");
    else
        printf("Je suis gros-boutiste.\n");
    return 0;
}

Notez bien que cette technique n’est pas entièrement fiable. En effet, rappelez-vous : d’une part, les deux boutismes présentés, s’ils sont les plus fréquents, ne sont pas les seuls et, d’autre part, la présence de bits de bourrage au sein du type entier, bien que rare, pourrait fausser le résultat.

Le boutisme des constantes octales et hexadécimales

Suivant ce qui vient de vous être présenté, peut-être vous êtes vous posé la question suivante : si les boutismes varient d’une machine à l’autre, alors que vaut finalement la constante 0x01 ? En effet, suivant les boutismes employés, celle-ci peut valoir 1 ou 16.

Heureusement, cette question est réglée par la norme 1 : le boutisme ne change rien à l’écriture des constantes octales et hexadécimales, elle est toujours réalisée suivant la convention classique des chiffres de poids fort en premier. Ainsi, la constante 0x01 vaut 1 et la constante 0x8000 vaut 32768 (en non signé).

De même, les indicateurs de conversion x , X et o affichent toujours leurs résultats suivant cette écriture.

 printf("%02x\n", 1);
Résultat
 01

  1. ISO/IEC 9899:201x, doc. N1570, § 6.4.4.1, Integer constants, al. 4, p. 63.

    ↩

Les fonctions memset, memcpy, memmove et memcmp

Pour terminer ce chapitre, il nous reste à vous présenter quatre fonctions que nous avions passé sous silence lors de notre présentation de l’en-tête <string.h> : memset() , memcpy() , memmove() et memcmp() . Bien qu’elles soient définies dans cet en-tête, ces fonctions ne sont pas véritablement liées aux chaînes de caractères et opèrent en vérité sur les multiplets composant les objets. De telles modifications impliquant la représentation des types, nous avons attendu ce chapitre pour vous en parler.

La fonction memset

void *memset(void *obj, int val, size_t taille);

La fonction memset() initialise les taille premiers multiplets de l’objet référencé par obj avec la valeur val (qui est convertie vers le type unsigned char ). Cette fonction est très utile pour (ré)initialiser les différents éléments d’un aggrégats sans devoir recourir à une boucle.

#include <stdio.h>
#include <string.h>
int main(void)
{
    int tab[] = { 10, 20, 30, 40, 50 };
    memset(tab, 0, sizeof tab);
    for (size_t i = 0; i < sizeof tab / sizeof tab[0]; ++i)
        printf("%zu : %d\n", i, tab[i]);
    return 0;
}
Résultat
0 : 0
1 : 0
2 : 0
3 : 0
4 : 0

Faites attention ! Manipuler ainsi les objets byte par byte nécessite de connaître leur représentation. Dans un souci de portabilité, on ne devrait donc pas utiliser memset sur des pointeurs ou des flottants. De même, pour éviter d’obtenir les représentations problématiques dans le cas d’entiers signés, il est conseillé de n’employer cette fonction que pour mettre ces derniers à zéro.

Les fonctions memcpy et memmove

void *memcpy(void *destination, void *source, size_t taille);
void *memmove(void *destination, void *source, size_t taille);

Les fonctions memcpy() et memmove() copient toutes deux les taille premiers multiplets de l’objet pointé par source vers les premiers multiplets de l’objet référencé par destination . La différence entre ces deux fonctions est que memcpy() ne doit être utilisée qu’avec deux objets qui ne se chevauchent pas (autrement dit, les deux pointeurs ne doivent pas accéder ou modifier une même zone mémoire ou une partie d’une même zone mémoire) alors que memmove() ne souffre pas de cette restriction.

#include <stdio.h>
#include <string.h>
int main(void)
{
    int n = 10;
    int m;
    memcpy(&m , &n, sizeof n);
    printf("m = n = %d\n", m);
    memmove(&n, ((unsigned char *)&n) + 1, sizeof n - 1);
    printf("n = %d\n", n);
    return 0;
}
Résultat
m = n = 10
n = 0

L’utilisation de memmove() amène quelques précisions : nous copions ici les trois derniers multiplets de l’objet n vers ses trois premiers multiplets. Étant donné que notre machine est petit-boutiste, les trois derniers multiplets sont à zéro et la variable n a donc pour valeur finale zéro.

La fonction memcmp

int memcmp(void *obj1, void *obj2, size_t taille);

La fonction memcmp() compare les taille premiers multiplets des objets référencés par obj1 et obj2 et retourne un nombre entier inférieur, égal ou supérieur à zéro suivant que la valeur du dernier multiplet comparé (convertie en unsigned char ) de obj1 est inférieure, égales ou supérieure à celle du dernier multiplet comparé de obj2 .

#include <stdio.h>
#include <string.h>
int main(void)
{
    int n = 10;
    int m = 10;
    int o = 20;
    if (memcmp(&n, &m, sizeof n) == 0)
        printf("n et m sont égaux\n");
    if (memcmp(&n, &o, sizeof n) != 0)
        printf("n et o ne sont pas égaux\n");
    return 0;
}
Résultat
n et m sont égaux
n et o ne sont pas égaux

Dit autrement, la fonction memcmp() retourne zéro si les deux portions comparées ont la même représentation.

En résumé
  1. les entiers non signés sont représentés sous forme d’une somme de puissance de deux ;
  2. il existe trois représentations possibles pour les entiers signés, mais la plus fréquente est celle en complément à deux ;
  3. les types entiers peuvent contenir des bits de bourrage sauf le type char ;
  4. les types flottants sont représentés sous forme d’un signe, d’un exposant et d’une mantisse, mais sont susceptibles d’engendrer des approximations et des pertes de précision ;
  5. la représentation des pointeurs est indéterminée ;
  6. le boutisme utilisé dépend de la machine cible ;
  7. les constantes sont toujours écrites suivant la représentation gros-boutiste.


Les limites des types - Le langage C • Tutoriels • Zeste de Savoir

Les limites des types

Les limites des types

Au début de ce cours, nous vous avons précisés que tous les types avaient des bornes et qu’en conséquence, ils ne pouvaient stocker qu’un intervalle définit de valeurs. Nous vous avons également indiqué que la norme du langage imposait des minimas pour ces intervalles qui, pour rappel, sont les suivants.

Type | Minimum | Maximum
_Bool | 0 | 1
signed char | -127 | 127
unsigned char | 0 | 255
short | -32 767 | 32 767
unsigned short | 0 | 65 535
int | -32 767 | 32 767
unsigned int | 0 | 65 535
long | -2 147 483 647 | 2 147 483 647
unsigned long | 0 | 4 294 967 295
long long | -9 223 372 036 854 775 807 | 9 223 372 036 854 775 807
unsigned long long | 0 | 18 446 744 073 709 551 615
float | -1 × 1037 | 1 × 1037
double | -1 × 1037 | 1 × 1037
long double | -1 × 1037 | 1 × 1037

Toutefois, mise à part pour les types _Bool et char , les bornes des types vont le plus souvent au-delà de ces minimums garantis. Cependant, pour pouvoir manipuler nos types dans leur limites, encore faut-il les connaître. Heureusement pour nous, la norme nous fourni deux en-têtes définissant des macroconstantes correspondant aux limites des types entiers et flottants : <limits.h> et <float.h> .

L’en-tête <limits.h>

Type | Minimum | Maximum
signed char | SCHAR_MIN | SCHAR_MAX
unsigned char | 0 | UCHAR_MAX
short | SHRT_MIN | SHRT_MAX
unsigned short | 0 | USHRT_MAX
int | INT_MIN | INT_MAX
unsigned int | 0 | UINT_MAX
long | LONG_MIN | LONG_MAX
unsigned long | 0 | ULONG_MAX
long long | LLONG_MIN | LLONG_MAX
unsigned long long | 0 | ULLONG_MAX

Remarquez qu’il n’y a pas de macroconstantes pour le minimum des types entiers non signés puisque celui-ci est toujours zéro.

À ces macroconstantes, il faut également ajouter CHAR_BIT qui vous indique le nombre bits composant un multiplet. Celle-ci a pour valeur minimale huit, c’est-à-dire qu’en C, un multiplet ne peut avoir moins de huit bits .

L’exemple suivant utilise ces macroconstantes et affiche leur valeur.

#include <limits.h>
#include <stdio.h>
int
main(void)
{
    printf("Un multiplet se compose de %d bits.\n", CHAR_BIT);
    printf("signed char : min = %d ; max = %d.\n", SCHAR_MIN, SCHAR_MAX);
    printf("unsigned char : min = 0 ; max = %u.\n", UCHAR_MAX);
    printf("short : min = %d ; max = %d.\n", SHRT_MIN, SHRT_MAX);
    printf("unsigned short : min = 0 ; max = %u.\n", USHRT_MAX);
    printf("int : min = %d ; max = %d.\n", INT_MIN, INT_MAX);
    printf("unsigned int : min = 0 ; max = %u.\n", UINT_MAX);
    printf("long : min = %ld ; max = %ld.\n", LONG_MIN, LONG_MAX);
    printf("unsigned long : min = 0 ; max = %lu.\n", ULONG_MAX);
    printf("long long : min = %lld ; max = %lld.\n", LLONG_MIN, LLONG_MAX);
    printf("unsigned long long : min = 0 ; max = %llu.\n", ULLONG_MAX);
    return 0;
}
Résultat
Un multiplet se compose de 8 bits.
signed char : min = -128 ; max = 127.
unsigned char : min = 0 ; max = 255.
short : min = -32768 ; max = 32767.
unsigned short : min = 0 ; max = 65535.
int : min = -2147483648 ; max = 2147483647.
unsigned int : min = 0 ; max = 4294967295.
long : min = -9223372036854775808 ; max = 9223372036854775807.
unsigned long : min = 0 ; max = 18446744073709551615.
long long : min = -9223372036854775808 ; max = 9223372036854775807.
unsigned long long : min = 0 ; max = 18446744073709551615.

Bien entendu, les valeurs obtenues dépendent de votre machine, celles-ci sont simplement données à titre d’illustration.

L’en-tête <float.h>

Les types flottants amènent une subtilité supplémentaire : ceux-ci disposent en vérité de quatre bornes : le plus grand nombre représentable, le plus petit nombre représentable (qui est l’opposé du précédent), la plus petite partie fractionnaire représentable (autrement dit, le nombre le plus proche de zéro représentable) et son opposé. Dit autrement, les bornes des types flottants peuvent être schématisées comme suit.

-MAX             -MIN   0   MIN              MAX
  +----------------+    +    +----------------+

Où MAX représente le plus grand nombre représentable et MIN le plus petit nombre représentable avant zéro. Étant donné que deux des bornes ne sont finalement que l’opposé des autres, deux macroconstantes sont suffisantes pour chaque type flottant.

Type | Maximum | Minimum de la partie fractionnaire
float | FLT_MAX | FLT_MIN
double | DBL_MAX | DBL_MIN
long double | LDBL_MAX | LDBL_MIN
#include <float.h>
#include <stdio.h>
int
main(void)
{
    printf("float : min = %e ; max = %e.\n", FLT_MIN, FLT_MAX);
    printf("double : min = %e ; max = %e.\n", DBL_MIN, DBL_MAX);
    printf("long double : min = %Le ; max = %Le.\n", LDBL_MIN, LDBL_MAX);
    return 0;
}
Résultat
float : min = 1.175494e-38 ; max = 3.402823e+38.
double : min = 2.225074e-308 ; max = 1.797693e+308.
long double : min = 3.362103e-4932 ; max = 1.189731e+4932.

Les dépassements de capacité

Nous savons à présent comment obtenir les limites des différents types de base, c’est bien. Toutefois, nous ignorons toujours quand la limite d’un type est dépassée et ce qu’il se passe dans un tel cas.

Le franchissement de la limite d’un type est appelé un dépassement de capacité ( overflow ou underflow en anglais, suivant que la limite maximale ou minimale est outrepassée). Un dépassement survient lorsqu’une opération produit un résultat dont la valeur n’est pas représentable par le type de l’expression ou lorsqu’une valeur est convertie vers un type qui ne peut la représenter.

Ainsi, dans l’exemple ci-dessous, l’expression INT_MAX + 1 provoquent un dépassement de capacité, de même que la conversion (implicite) de la valeur INT_MAX vers le type signed char .

signed char c;
if (INT_MAX + 1) /* Dépassement. */
    c = INT_MAX; /* Dépassement. */
c = SCHAR_MAX; /* Ok. */

Gardez en mémoire que des conversions implicites ont lieu lors des opérations. Revoyez le chapitre sur les opérations mathématiques si cela vous paraît lointain.

D’accord, mais que se passe-t-il dans un tel cas ?

C’est ici que le bât blesse : ce n’est pas précisé. :-°
Ou, plus précisément, cela dépend des situations.

Dépassement lors d’une opération

Les types entiers

Les entiers signés

Si une opération travaillant avec des entiers signés dépasse la capacité du type du résultat, le comportement est indéfini. Dans la pratique, il y a trois possibilités :

  1. La valeur boucle, c’est-à-dire qu’une fois une limite atteinte, la valeur revient à la seconde. Cette solution est la plus fréquente et s’explique par le résultat de l’arithmétique en complément à deux. En effet, à supposer qu’un int soit représenté sur 32 bits :

    • INT_MAX + 1 se traduit par 0111 1111 1111 1111 1111 1111 1111 1111 + 0000 0000 0000 0000 0000 0000 0000 0001 , ce qui donne 1000 0000 0000 0000 0000 0000 0000 0000 , soit INT_MIN ; et
    • INT_MIN - 1 se traduit par 1000 0000 0000 0000 0000 0000 0000 0000 + 1111 1111 1111 1111 1111 1111 1111 1111 ce qui donne 0111 1111 1111 1111 1111 1111 1111 1111 , soit INT_MAX .
  2. La valeur sature, c’est-à-dire qu’une fois une limite atteinte, la valeur reste bloquée à celle-ci.
  3. Le processeur considère l’opération comme invalide et stoppe l’exécution du programme.
Les entiers non signés

Si une opération travaillant avec des entiers non signés dépasse la capacité du type du résultat, alors il n’y a à proprement parler pas de dépassement et la valeur boucle. Autrement dit, l’expression UINT_MAX + 1 donne par exemple toujours zéro.

Les types flottants

Si une opération travaillant avec des flottants dépasse la capacité du type du résultat, alors, comme pour les entiers signés, le comportement est indéfini. Toutefois, les flottants étant généralement représenté suivant la norme IEEE 754, le résultat sera souvent le suivant :

  1. En cas de dépassement de la borne maximale ou minimale, le résultat sera égal à l’infini (positif ou négatif).
  2. En cas de dépassement de la limite de la partie fractionnaire, le résultat sera arrondi à zéro.

Néanmoins, gardez à l’esprit que ceci n’est pas garantit par la norme. Il est donc parfaitement possible que votre programme soit tout simplement arrêté.

Dépassement lors d’une conversion

Valeur entière vers entier signé

Si la conversion d’une valeur entière vers un type signé produit un dépassement, le résultat n’est pas déterminé.

Valeur entière vers entier non signé

Dans le cas où la conversion d’une valeur entière vers un type non signé, la valeur boucle, comme précisé dans le cas d’une opération impliquant des entiers non signés.

Valeur flottante vers entier

Lors d’une conversion d’une valeur flottante vers un type entier (signé ou non signé), la partie fractionnaire est ignorée. Si la valeur restante n’est pas représentable, le résultat est indéfini ( y compris pour les types non signés, donc !).

Valeur entière vers flottant

Si la conversion d’une valeur entière vers un type flottant implique un dépassement, le comportement est indéfini.

Notez que si le résultat n’est pas exactement représentable (rappelez-vous des pertes de précision) la valeur sera approchée, mais cela ne constitue pas un dépassement.

Valeur flottante vers flottant

Enfin, si la conversion d’une valeur flottante vers un autre type flottant produit un dépassement, le comportement est indéfini.

Comme pour le cas d’une conversion d’un type entier vers un type flottant, si le résultat n’est pas exactement représentable, la valeur sera approchée.

Gérer les dépassements entiers

Nous savons à présent ce qu’est un dépassement, quand ils se produisent et dans quel cas ils sont problématiques (en vérité, potentiellement tous, puisque même si la valeur boucle, ce n’est pas forcément ce que nous souhaitons). Reste à présent pour nous à les gérer, c’est-à-dire les détecter et les éviter. Malheureusement pour nous, la bibliothèque standard ne nous fourni aucun outil à cet effet, mise à part les macroconstantes qui vous ont été présentées. Aussi il va nous être nécessaire de réaliser nos propres outils.

Préparation

Avant de nous lancer dans la construction de ces fonctions, il va nous être nécessaire de déterminer leur forme. En effet, si, intuitivement, il vous vient sans doute à l’esprit de créer des fonctions prenant deux paramètres et fournissant un résultat, reste le problème de la gestion d’erreur.

int safe_add(int a, int b);

En effet, comment précise-t-on à l’utilisateur qu’un dépassement a été détecté, toutes les valeurs du type int pouvant être retournées ? Eh bien, pour résoudre ce souci, nous allons recourir dans les exemples qui suivent à la variable errno , à laquelle nous affecteront la valeur ERANGE en cas de dépassement. Ceci implique qu’elle soit mise à zéro avant tout appel à ces fonctions (revoyez le premier chapitre sur la gestion d’erreur si cela ne vous dit rien).

Par ailleurs, afin que ces fonctions puissent éventuellement être utilisées dans des suites de calculs, nous allons faire en sorte qu’elle retourne la borne maximale ou minimale en cas de dépassement.

Ceci étant posé, passons à la réalisation de ces fonctions. :)

L’addition

Afin d’empêcher un dépassement, il va nous falloir le détecter et ceci, bien entendu, à l’aide de condition qui ne doivent pas elle-même produire de dépassement. Ainsi, ce genre de condition est à proscrire : if (a + b > INT_MAX) . Pour le cas d’une addition deux cas de figures se présentent :

  1. b est positif et il nous faut alors vérifier que la somme ne dépasse pas le maximum.
  2. b est négatif et il nous faut dans ce cas déterminer si la somme dépasse ou non le minimum.

Ceci peut être contrôlé en vérifiant que a n’est pas supérieur ou inférieur à, respectivement, MAX - b et MIN - b . Ainsi, il nous est possible de rédiger une fonction comme suit.

int safe_add(int a, int b)
{
    int err = 1;
    if (b >= 0 && a > INT_MAX - b)
        goto overflow;
    else if (b < 0 && a < INT_MIN - b)
        goto underflow;
    return a + b;
underflow:
    err = -1;
overflow:
    errno = ERANGE;
    return err > 0 ? INT_MAX : INT_MIN;
}

Contrôlons à présent son fonctionnement.

#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
/* safe_add() */
static void test_add(int a, int b)
{
    errno = 0;
    printf("%d + %d = %d", a, b, safe_add(a, b));
    printf(" (%s).\n", strerror(errno));
}
int main(void)
{
    test_add(INT_MAX, 1);
    test_add(INT_MIN, -1);
    test_add(INT_MIN, 1);
    test_add(INT_MAX, -1);
    return 0;
}

2147483647 + 1 = 2147483647 (Numerical result out of range).
-2147483648 + -1 = -2147483648 (Numerical result out of range).
-2147483648 + 1 = -2147483647 (Success).
2147483647 + -1 = 2147483646 (Success).

La soustraction

Pour la soustraction, le principe est le même que pour l’addition si ce n’est que les tests doivent être quelques peu modifiés. En guise d’exercice, essayez de trouver la solution par vous-même. ;)

Afficher/Masquer le contenu masqué
int safe_sub(int a, int b)
{
    int err = 1;
    if (b >= 0 && a < INT_MIN + b)
        goto underflow;
    else if (b < 0 && a > INT_MAX + b)
        goto overflow;
    return a - b;
underflow:
    err = -1;
overflow:
    errno = ERANGE;
    return err > 0 ? INT_MAX : INT_MIN;
}

Mais ?! Pourquoi ne pas simplement faire safe_add(a, -b) ?

Bonne question !

Cela a de quoi surprendre de prime à bord, mais l’opérateur de négation - peut produire un dépassement. En effet, souvenez-vous : la représentation en complément à deux ne dispose que d’une seule représentation pour zéro, du coup, il reste une représentation qui est possiblement invalide où seul le bit de signe est à un. Cependant, le plus souvent, cette représentation est utilisée pour fournir un nombre négatif supplémentaire. Or, si c’est le cas, il y a une asymétrie entre la borne inférieure et supérieure et la négation de la limite inférieure produira un dépassement. Il nous est donc nécessaire de prendre ce cas en considération.

La négation

À ce sujet, pour réaliser cette opération, deux solutions s’offrent à nous :

  1. Vérifier si l’opposé de la borne supérieure est plus grand que la borne inférieure (autrement dit que -INT_MAX > INT_MIN ) et, si c’est le cas, vérifier que le paramètre n’est pas la borne inférieure ; ou
  2. Vérifier sub(0, a) où a est l’opérande qui doit subir la négation.

Notre préférence allant à la seconde solution, notre fonction sera donc la suivante.

int safe_neg(int a)
{
    return safe_sub(0, a);
}

La multiplication

Pour la multiplication, cela se corse un peu, notamment au vu de ce qui a été dit précédemment au sujet de la représentation en complément à deux. Procédons par ordre :

  1. Tout d’abord, si l’un des deux opérandes est positif, nous devons vérifier que le minimum n’est pas atteint (puisque le résultat sera négatif) et, si les deux sont positifs, que le maximum n’est pas dépassé. Toutefois, vu que nous allons recourir à la division pour le déterminer, nous devons en plus vérifier que le diviseur n’est pas nul.
if (b > 0)
{
    if (a > 0 && a > INT_MAX / b)
        goto overflow;
    else if (a < 0 && a < INT_MIN / b)
        goto underflow;
}

Ensuite, si l’un des deux opérandes est négatif, nous devons effectuer la même vérification que précédemment et, si les deux sont négatifs, vérifier que le résultat ne dépasse pas le maximum. Toutefois, nous devons également faire attention à ce que les opérandes ne soient pas nuls ainsi qu’au cas de dépassement possible par l’expression INT_MIN / -1 (en cas de représentation en complément à deux).

else if (b < 0)
{
    if (a < 0 && a < INT_MAX / b)
        goto overflow;
    else if ((-INT_MAX > INT_MIN && b < -1) && a > INT_MIN / b)
        goto underflow;
}

Ce qui nous donne finalement la fonction suivante.

int safe_mul(int a, int b)
{
    int err = 1;
    if (b > 0)
    {
        if (a > 0 && a > INT_MAX / b)
            goto overflow;
        else if (a < 0 && a < INT_MIN / b)
            goto underflow;
    }
    else if (b < 0)
    {
        if (a < 0 && a < INT_MAX / b)
            goto overflow;
        else if ((-INT_MAX > INT_MIN && b < -1) && a > INT_MIN / b)
            goto underflow;
    }
    return a * b;
underflow:
    err = -1;
overflow:
    errno = ERANGE;
    return err > 0 ? INT_MAX : INT_MIN;
}

La division

Rassurez-vous, la division est nettement moins pénible à vérifier. :-°
En fait, il y a un seul cas problématique : si les deux opérandes sont INT_MIN et -1 et que l’opposé du maximum est inférieur à la borne minimale.

int safe_div(int a, int b)
{
    if (-INT_MAX > INT_MIN && b == -1 && a == INT_MIN)
    {
        errno = ERANGE;
        return INT_MIN;
    }
    return a / b;
}

Le modulo

Enfin, pour le modulo, les mêmes règles que celles de la division s’appliquent.

int safe_mod(int a, int b)
{
    if (-INT_MAX > INT_MIN && b == -1 && a == INT_MIN)
    {
        errno = ERANGE;
        return INT_MIN;
    }
    return a % b;
}

Gérer les dépassements flottants

La tâche se complique un peu avec les flottants, puisqu’en plus de tester les limites supérieures (cas d' overflow ), il faudra aussi s’intéresser à celles de la partie fractionnaire (cas d' underflow ).

L’addition

Comme toujours, il faut prendre garde à ce que les codes de vérification ne provoquent pas eux-mêmes des dépassements. Pour implémenter l’opération d’addition, on commence donc par se ramener au cas plus simple où on connaît le signe des opérandes a et b .

  • Si a et b sont de signes opposés (par exemple : a >= 0 et b >= 0 , alors il ne peut pas se produire d' overflow .
  • Si a et b sont de même signes, alors il ne peut pas se produire d' underflow .

Dans le premier cas, tester l' underflow revient à tester si a + b est censé (dans l’arithmétique usuelle) être compris entre -DBL_MIN et DBL_MIN . Il faut cependant exclure 0 de cet intervalle, de manière à ce que 1.0 + (-1.0) , par exemple, ne soit pas considéré comme un underflow . Si par exemple a >= 0 (et dans ce cas b <= 0 ), il suffit alors de tester si -DBL_MIN - a < b et a < DBL_MIN - b .

Dans le deuxième cas, il reste à vérifier l’absence d' overflow . Si par exemple a et b sont tous deux positifs, il s’agit de vérifier que a <= DBL_MAX - b .

/* Retourne a+b. */
/* En cas d'overflow, +-DBL_MAX est retourné (suivant le signe de a+b) */
/* En cas d'underflow, +- DBL_MIN est retourné */
/* errno est fixé en conséquence */
double safe_addf(double a, double b)
{
    int positive_result = a > -b;
    double ret_value = positive_result ? DBL_MAX : -DBL_MAX;
    if (a == -b)
        return 0;
    else if ((a < 0) != (b < 0))
    {
        /* On se ramène au cas où a <= 0 et b >= 0 */
        if (a <= 0)
        {
            double t = a;
            a = b;
            b = t;
        }
        if (-DBL_MIN - a < b && a < DBL_MIN - b)
            goto underflow;
    }
    else
    {
        int negative = 0;

        /* On se ramène au cas où a et b sont positifs */
        if (a < 0)
        {
            a = -a;
            b = -b;
            negative = 1;
        }
        if (a > DBL_MAX - b)
            goto overflow;
        if (negative)
        {
            a = -a;
            b = -b;
        }
    }
    return a + b;
underflow:
    ret_value = positive_result ? DBL_MIN : -DBL_MIN;
overflow:
    errno = ERANGE;
    return ret_value;
}

Quelques tests :

static void test_addf(double a, double b)
{
    errno = 0;
    printf("%e + %e = %e", a, b, safe_addf(a, b));
    printf(" (%s)\n", strerror(errno));
}
int main(void)
{
    test_addf(1, 1);
    test_addf(5, -6);
    test_addf(DBL_MIN, -3./2*DBL_MIN); /* underflow */
    test_addf(DBL_MAX, 1);
    test_addf(DBL_MAX, -DBL_MAX);
    test_addf(DBL_MAX, DBL_MAX); /* overflow */
    test_addf(-DBL_MAX, -1./2*DBL_MAX); /* overflow */
    return 0;
}
Résultat
1.000000e+00 + 1.000000e+00 = 2.000000e+00 (Success)
5.000000e+00 + -6.000000e+00 = -1.000000e+00 (Success)
2.225074e-308 + -3.337611e-308 = -2.225074e-308 (Numerical result out of range)
1.797693e+308 + 1.000000e+00 = 1.797693e+308 (Success)
1.797693e+308 + -1.797693e+308 = 0.000000e+00 (Success)
1.797693e+308 + 1.797693e+308 = 1.797693e+308 (Numerical result out of range)
-1.797693e+308 + -8.988466e+307 = -1.797693e+308 (Numerical result out of range)

On peut faire deux remarques sur la fonction safe_addf ci-dessus :

  • addf(DBL_MAX, 1) peut ne pas être considéré comme un overflow du fait des pertes de précision.
  • à l’inverse, si un underflow se déclare, cela ne veut pas forcément dire qu’il y a une erreur irrécupérable dans l’algorithme. Il est possible que les pertes de précision dans les représentations des flottants les causent elles-mêmes.

Pour être tout à fait rigoureux, il faut remarquer que des expressions telles que -DBL_MIN - a et DBL_MAX - b (avec a >= 0 et b >= 0 ) peuvent théoriquement générer respectivement un overflow et un underflow . Cela ne peut cependant se produire que sur des implémentations marginales des flottants, puisque cela signifierait que la partie fractionnaire (la mantisse) est représentée sur plus de bits que la différence entre les valeurs extrémales de l’exposant. Nous avons donc omis ces vérifications pour des raisons de concision.

La soustraction

Pour notre plus grand bonheur, étant donné qu’il n’y a pas d’asymétrie entre les bornes, la soustraction revient à faire safe_addf(a, -b) .

La multiplication

Heureusement, la multiplication est un peu plus simple que l’addition ! Comme tout à l’heure, on va essayer de contrôler la valeur des arguments de manière à savoir dans quel cas on se situe potentiellement ( underflow ou overflow ).

Ici, ce n’est plus le signe de a et de b qui va nous intéresser : au contraire, on va même le gommer en utilisant la valeur absolue. Cependant, la position de a et de b par rapport à +1 et -1 est essentielle :

  • si fabs(b) <= 1 , a * b peut produire un underflow mais pas d' overflow .
  • si fabs(b) >= 1 , a * b peut produit un overflow mais pas d' underflow .

La fonction fabs() est définie dans l’en-tête <math.h> et, comme son nom l’indique, retourne la valeur absolue de son argument. N’oubliez pas à cet égard de préciser l’option -lm lors de la compilation.

Ainsi, dans le premier cas, on vérifiera si fabs(a) < DBL_MIN / fabs(b) . L’expression DBL_MIN / fabs(b) ne peut ni produire d' underflow (puisque fabs(b) <= 1 ) ni d' overflow (puisque DBL_MIN <= fabs(b) ). De même, dans le deuxième cas, la condition pourra s’écrire fabs(a) > DBL_MAX / fabs(b) .

double safe_mulf(double a, double b)
{
    int different_signs = (a < 0) != (b < 0);
    double ret_value = different_signs ? -DBL_MAX : DBL_MAX;
    if (fabs(b) < 1)
    {
        if (fabs(a) < DBL_MIN / fabs(b))
            goto underflow;
    }
    else
    {
        if (fabs(a) > DBL_MAX / fabs(b))
            goto overflow;
    }
    return a * b;
underflow:
    ret_value = different_signs ? -DBL_MIN : DBL_MIN;
overflow:
    errno = ERANGE;
    return ret_value;
}

Quelques tests :

static void test_add(double a, double b)
{
    errno = 0;
    printf("%e + %e = %e", a, b, safe_addf(a, b));
    printf(" (%s)\n", strerror(errno));
}
int main(void)
{
    test_add(1, 1);
    test_add(5, -6);
    test_add(DBL_MIN, -3./2*DBL_MIN); /* underflow */
    test_add(DBL_MAX, 1);
    test_add(DBL_MAX, -DBL_MAX);
    test_add(DBL_MAX, DBL_MAX); /* overflow */
    test_add(-DBL_MAX, -1./2*DBL_MAX); /* overflow */
    return 0;
}
Résultat
-1.000000e+00 * -1.000000e+00 = 1.000000e+00 (Success)
2.000000e+00 * 3.000000e+00 = 6.000000e+00 (Success)
1.797693e+308 * 2.000000e+00 = 1.797693e+308 (Numerical result out of range)
2.225074e-308 * 5.000000e-01 = 2.225074e-308 (Numerical result out of range)
-3.337611e-308 * 6.666667e-01 = -2.225074e-308 (Success)
-8.988466e+307 * 1.500000e+00 = -1.348270e+308 (Success)
-8.988466e+307 * 3.000000e+00 = -1.797693e+308 (Numerical result out of range)

La division

La division ressemble sensiblement à la multiplication. On traite cependant en plus le cas où le dénominateur est nul.

double safe_divf(double a, double b)
{
    int different_signs = (a < 0) != (b < 0);
    double ret_value = different_signs ? -DBL_MAX : DBL_MAX;
    if (b == 0)
    {
        errno = EDOM;
        return ret_value;
    }
    else if (fabs(b) < 1)
    {
        if (fabs(a) > DBL_MAX * fabs(b))
            goto overflow;
    }
    else
    {
        if (fabs(a) < DBL_MIN * fabs(b))
            goto underflow;
    }
    return a / b;
underflow:
    ret_value = different_signs ? -DBL_MIN : DBL_MIN;
overflow:
    errno = ERANGE;
    return ret_value;
}

Ce chapitre aura été fastidieux, n’hésitez pas à vous poser après sa lecture et à tester les différents codes fourni afin de correctement appréhender les différentes limites.

En résumé
  1. Les limites des différents types sont fournies par des macroconstantes définies dans les en-têtes <limits.h> et <float.h> ;
  2. Les types flottants disposent de quatre limites et non de deux ;
  3. Un dépassement de capacité se produit soit quand une opération produit un résultat hors des limites du type de son expression ou lors de la conversion d’une valeur vers un type qui ne peut représenter celle-ci ;
  4. En cas de dépassement, le comportement est indéfini sauf pour les calculs impliquant des entiers non signés et pour les conversions d’un type entier vers un type entier non signé, auquel cas la valeur boucle.
  5. La bibliothèque standard ne fournit aucun outils permettant de détecter et/ou de gérer ces dépassements.


Manipulation des bits - Le langage C • Tutoriels • Zeste de Savoir

Manipulation des bits

Les opérateurs de manipulation des bits

Présentation

Le langage C définit six opérateurs permettant de manipuler les bits :

  • l’opérateur « et » : & ;
  • l’opérateur « ou inclusif » : | ;
  • l’opérateur « ou exclusif » : ^ ;
  • l’opérateur de négation ou de complément : ~ ;
  • l’opérateur de décalage à droite : >> ;
  • l’opérateur de décalage à gauche : << .

Veillez à ne pas confondre les opérateurs de manipulations des bits « et » ( & ) et « ou inclusif » ( | ) avec leurs homologues « et » ( && ) et « ou » ( || ) logiques. Il s’agit d’opérateurs totalement différent au même titre que les opérateurs d’affectation ( = ) et d’égalité ( == ). De même, l’opérateur de manipulations des bits « et » ( & ) n’a pas de rapport avec l’opérateur d’adressage ( & ), ce dernier n’utilisant qu’un opérande.

Notez que tous ces opérateurs travaillent uniquement sur des entiers .

Si néanmoins vous souhaitez modifier la représentation d’un type flottant (ce que nous vous déconseillons), vous pouvez accéder à ses bits à l’aide d’un pointeur sur unsigned char (revoyez le chapitre sur l’allocation dynamique si cela ne vous dit rien).

Les opérateurs « et », « ou inclusif » et « ou exclusif »

Chacun de ces trois opérateurs attend deux opérandes entiers et produit un nouvel entier en appliquant une table de vérité à chaque paire de bits formée à partir des bits des deux nombres de départs. Plus précisément :

  • L’opérateur « et » ( & ) donnera 1 si les deux bits de la paire sont à 1 ;
  • L’opérateur « ou inclusif » ( | ) donnera 1 si au moins un des deux bits de la paire est à 1 ;
  • L’opérateur « ou exclusif » ( ^ ) donnera 1 si un seul des deux bits de la paire est à 1.

Ceci est résumé dans le tableau ci-dessous, donnant le résultat des trois opérateurs pour chaque paire de bits possible.

Bit 1 | Bit 2 | Opérateur « et » | Opérateur « ou inclusif » | Opérateur « ou exclusif »
0 | 0 | 0 | 0 | 0
1 | 0 | 0 | 1 | 1
0 | 1 | 0 | 1 | 1
1 | 1 | 1 | 1 | 0

L’exemple ci-dessous utilise ces trois opérateurs. Comme vous le voyez, les bits des deux opérandes sont pris un à un pour former des paires et chacune d’elle se voit appliquer la table de vérité correspondante afin de produire un nouveau nombre entier.

#include <stdio.h>
int main(void)
{
    int a = 0x63; /* 0x63 == 99 == 0110 0011 */
    int b = 0x2A; /* 0x2A == 42 == 0010 1010 */
    /* 0110 0011 & 0010 1010 == 0010 0010 == 0x22 == 34 */
    printf("%2X\n", a & b);
    /* 0110 0011 | 0010 1010 == 0110 1011 == 0x6B == 107 */
    printf("%2X\n", a | b);
    /* 0110 0011 ^ 0010 1010 == 0100 1001 == 0x49 == 73 */
    printf("%2X\n", a ^ b);
    return 0;
}
Résultat
22
6B
49

L’opérateur de négation ou de complément

L’opérateur de négation a un fonctionnement assez simple : il inverse les bits de son opérande (les uns deviennent des zéros et les zéros des uns).

#include <stdio.h>
int
main(void)
{
    unsigned a = 0x7F; /* 0111 1111 */
    /* ~0111 1111 == 1000 0000 */
    printf("%2X\n", ~a);
    return 0;
}
Résultat
FFFFFF80

Notez que tous les bits ont été inversés, d’où le nombre élevé que nous obtenons puisque les bits de poids forts ont été mis à un. Ceci nous permet par ailleurs de constater que, sur notre machine, il y a visiblement quatre octets qui sont utilisés pour représenter la valeur d’un objet de type unsigned int .

N’oubliez pas que les représentations entières signées ont chacune une représentation qui est susceptible d’être invalide. Les opérateurs de manipulation des bits vous permettant de modifier directement la représentation, vous devez éviter d’obtenir ces dernières.

Ainsi, les exemples suivants sont susceptibles de produire des valeurs incorrectes (à supposer que la taille du type int soit de quatre octets sans bits de bourrages).

/* Invalide en cas de représentation en complément à deux ou signe et magnitude */
int a = ~0x7FFFFFFF;
/* Idem */
int b = 0x00000000 | 0x80000000;
/* Invalide en cas de représentation en complément à un */
int c = ~0;
/* Idem */
int d = 0x11110000 ^ 0x00001111;

Notez toutefois que les entiers non signés , eux, ne subissent pas ces restrictions.

Les opérateurs de décalage

Les opérateurs de décalage, comme leur nom l’indique, décalent la valeur des bits d’un objet d’une certaine quantité, soit vers la gauche (c’est-à-dire vers le bit de poids fort), soit vers la droite (autrement dit, vers le bit de poids faible). Ils attendent deux opérandes : le nombre dont les bits doivent être décalés et la grandeur du décalage.

Un décalage ne peut être négatif ni être supérieur ou égal au nombre de bits composant l’objet décalé. Ainsi, si le type int utilise 32 bits (sans bits de bourrage), le décalage ne peut être plus grand que 31.

L’opérateur de décalage à gauche

L’opérateur de décalage à gauche translate la valeur des bits vers le bit de poids forts. Les bits de poids faibles perdant leur valeur durant l’opération sont mis à zéro. Techniquement, l’opération de décalage à gauche revient à calculer la valeur de l’expression a × 2y.

#include <stdio.h>
int
main(void)
{
    /* 0000 0001 << 2 == 0000 0100 */
    int a = 1 << 2;
    /* 0010 1010 << 2 == 1010 1000 */
    int b = 42 << 2;
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
Résultat
a = 4, b = 168

Le premier opérande ne peut être un nombre négatif.

L’opération de décalage à gauche revenant à effectuer une multiplication, celle-ci est soumise au risque de dépassement de capacité que nous verrons au chapitre suivant.

L’opérateur de décalage à droite

L’opérateur de décalage à droite translate la valeur des bits vers le bit de poids faible. Dans le cas où le premier opérande est un entier non signé ou un entier signé positif , les bits de poids forts perdant leur valeur durant l’opération sont mis à zéro. Si en revanche il s’agit d’un nombre signé négatif , les bits perdant leur valeur se voient mis à zéro ou un suivant la machine cible. Techniquement, l’opération de décalage à droite revient à calculer la valeur de l’expression a / 2y.

#include <stdio.h>
int
main(void)
{
    /* 0001 0000 >> 2 == 0000 0100 */
    int a = 16 >> 2;
    /* 0010 1010 >> 2 == 0000 1010 */
    int b = 42 >> 2;
    printf("a = %d, b = %d\n", a, b);
    return 0;
}
Résultat
a = 4, b = 10

Dans le cas où une valeur est translatée au-delà du bit de poids faible, elle est tout simplement perdue.

Opérateurs combinés

Enfin, sachez que, comme pour les opérations arithmétiques usuelles, les opérateurs de manipulation des bits disposent d’opérateurs combinés réalisant une affectation et une opération.

Opérateur combiné | Équivalent à
variable &= nombre variable |= nombre variable ^= nombre variable <<= nombre variable >>= nombre | variable = variable & nombre variable = variable | nombre variable = variable ^ nombre variable = variable << nombre variable = variable >> nombre

Masques et champs de bits

Les masques

Une des utilisations fréquentes des opérateurs de manipulations des bits est l’emploi de masques. Un masque est un ensemble de bits appliqué à un second ensemble de même taille lors d’une opération de manipulation des bits (plus précisément, uniquement les opérations & , | et ^ ) en vue soit de sélectionner un sous-ensemble, soit de modifier un sous-ensemble.

Modifier la valeur d’un bit

Mise à zéro

Cette définition doit probablement vous paraître quelque peu abstraite, aussi, prenons un exemple.

unsigned short n;

Nous disposons d’une variable n de type unsigned short (que nous supposerons composées de deux octets pour nos exemples) et souhaiterions mettre le bit de poids fort à zéro.

Une solution consiste à appliquer les opérateurs de manipulation des bits afin d’obtenir la valeur voulue. Étant donné que nous désirons mettre un bit à zéro, nous pouvons déjà abandonner l’opérateur | au vu de sa table de vérité. Également, l’opérateur ^ ne convient pas tout à fait puisqu’il inverserait la valeur du bit au lieu de la mettre à zéro. Il nous reste donc l’opérateur & .

Avec cet opérateur, il nous est possible d’utiliser une valeur qui nous donnera le bon résultat. Cette valeur, de même taille que celle de la variable n , est précisément un masque qui va « cacher », « masquer » une partie de la valeur.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    unsigned short n;
    if (scanf("%hx", &n) != 1)
    {
        perror("scanf");
        return EXIT_FAILURE;
    }
    printf("%X\n", n & 0x7FFF);
    return 0;
}
Résultat
8FFF
FFF
7F
7F

Comme vous le voyez, l’opérateur & peut être utilisé pour sélectionner une partie de la valeur de n en mettant à un les bits que nous souhaitons garder (en l’occurrence tous sauf le bit de poids fort) et les autres à zéro.

Mise à un

À l’inverse, les opérateurs de manipulation des bits peuvent être employés pour mettre un ou plusieurs bits à un. Dans ce cas, c’est l’opérateur & qui ne convient pas au vu de sa table de vérité.

Si nous reprenons notre exemple précédent et que nous souhaitons modifier la valeur de la variable n de sorte de mettre à un le bit de signe, nous pouvons procéder comme suit.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    unsigned short n;
    if (scanf("%hx", &n) != 1)
    {
        perror("scanf");
        return EXIT_FAILURE;
    }
    printf("%X\n", n | 0x8000);
    return 0;
}
Résultat
FFF
8FFF
7F
807F

Comme vous le voyez, l’opérateur | peut être utilisé de la même manière dans ce cas ci à l’aide d’un masque dont les bits qui doivent être mis à un sont… à un.

Les champs de bits

Mise en situation

Une autre utilisation des opérateurs de manipulation des bits est le compactage de données entières.

Imaginons que nous souhaitions stocker la date courante sous la forme de trois entiers : l’année, le mois et le jour. La première solution qui vous viendra à l’esprit sera probablement de recourir à une structure, par exemple comme celle ci-dessous, ce qui est un bon réflexe.

struct date {
    unsigned char jour;
    unsigned char mois;
    unsigned short annee;
};

Toutefois, nous gaspillons finalement de la mémoire avec ce système. En effet, techniquement, nous aurions besoin de 12 bits pour stocker l’année (afin d’avoir un peu de marge jusque l’an 4095 :p ), 4 pour le mois et 5 pour le jour, ce qui nous fait un total de 21 bits contre 32 pour notre structure (à supposer que le type short fasse deux octets et le type char un octet), sans compter les multiplets de bourrage (revoyez le chapitre sur les structures si cela ne vous dit rien ;) ).

Ceci n’est pas gênant dans la plupart des cas, mais cela peut le devenir si la mémoire disponible vient à manquer ou si cette structure est amenée à être créée un grand nombre de fois.

Avec les opérateurs de manipulations des bits , il nous est possible de stocker ces trois champs dans un tableau de trois unsigned char afin d’économiser de la place.

#include <stdio.h>
#include <stdlib.h>
static void modifie_jour(unsigned char *date, unsigned jour)
{
    /* Nous stockons le jour (cinq bits). */
    date[0] |= jour;
}
static void modifie_mois(unsigned char *date, unsigned mois)
{
    /* Nous ajoutons les trois premiers bits du mois après ceux du jour. */
    date[0] |= (mois & 0x07) << 5;
    /* Puis le bit restant dans le second octet. */
    date[1] |= (mois >> 3);
}
static void modifie_annee(unsigned char *date, unsigned annee)
{
    /* Nous ajoutons sept bits de l'année après le dernier bit du mois. */
    date[1] |= (annee & 0x7F) << 1;
    /* Et ensuite les cinq restants. */
    date[2] |= (annee) >> 7;
}
static unsigned calcule_jour(unsigned char *date)
{
    return date[0] & 0x1F;
}
static unsigned calcule_mois(unsigned char *date)
{
    return (date[0] >> 5) | ((date[1] & 0x1) << 3);
}
static unsigned calcule_annee(unsigned char *date)
{
    return (date[1] >> 1) | (date[2] << 7);
}
int
main(void)
{
    unsigned char date[3] = { 0 }; /* Initialisation à zéro. */
    unsigned jour, mois, annee;
    printf("Entrez une date au format jj/mm/aaaa : ");
    if (scanf("%u/%u/%u", &jour, &mois, &annee) != 3) {
        perror("fscanf");
        return EXIT_FAILURE;
    }
    modifie_jour(date, jour);
    modifie_mois(date, mois);
    modifie_annee(date, annee);
    printf("Le %u/%u/%u\n", calcule_jour(date), calcule_mois(date), calcule_annee(date));
    return 0;
}
Résultat
Entrez une date au format jj/mm/aaaa : 31/12/2042
Le 31/12/2042

Cet exemple amène quelques explications.
Une fois les trois valeurs récupérées, il nous faut les compacter dans le tableau d' unsigned char :

  1. Pour le jour, c’est assez simple, nous incorporons ses cinq bits à l’aide de l’opérateur | (les trois éléments du tableau étant à zéro au début, cela ne pose pas de problème).
  2. Pour le mois, seuls trois bits étant encore disponibles, il va nous falloir répartir ceux-ci sur deux éléments du tableau. Tout d’abord, nous sélectionnons les trois premiers bits à l’aide du masque 0x07 , nous les décalons ensuite de cinq bits vers la gauche (afin de ne pas écraser les cinq bits du jour) et, enfin, nous les ajoutons à l’aide de l’opérateur | . Le dernier bit est lui stocké dans le second élément et est sélectionné à l’aide d’un décalage vers la droite afin d’éliminer les trois premiers bits (qui ont déjà été traité).
  3. Pour l’année, nous utilisons la même technique que pour le mois : nous sélectionnons les sept premiers bits à l’aide du masque 0x7F , les décalons d’un bit vers la gauche en vue de ne pas écraser le bit du mois et les intégrons avec l’opérateur | . Les cinq bits restants sont ensuite insérés en recourant préalablement à un décalage de sept bit vers la droite.

Présentation

Comme vous le voyez, si nous gagnons effectivement de la place en mémoire, nous y perdons en temps de calcul et, plus important, notre code est nettement plus complexe. C’est la raison pour laquelle cette méthode n’est employée que dans le cas de contraintes particulières.

Bien entendu, nous pourrions recourir à des fonctions ou à des macrofonctions pour simplifier la lecture du code, toutefois, nous ne ferions que reporter la difficulté de compréhension sur ces dernières. Heureusement, en vue de simplifier ce type d’écritures, le langage C met à notre disposition les champs de bits .

Un champ de bits est une structure composée exclusivement de champs de type int ou unsigned int dont la taille en bits de chacun est précisée. Cette taille ne peut être supérieure à la taille en bits du type int . L’exemple ci-dessous défini une structure composée de trois champs de bits , a , b et c de respectivement un, deux et trois bits .

struct champ_de_bits
{
    unsigned a : 1;
    unsigned b : 2;
    unsigned c : 3;
};

Ainsi, notre exemple précédent peut être réécrit comme ceci.

#include <stdio.h>
#include <stdlib.h>
struct date {
    unsigned jour : 5;
    unsigned mois : 4;
    unsigned annee : 12;
};
int
main(void)
{
    unsigned jour, mois, annee;
    printf("Entrez une date au format jj/mm/aaaa : ");
    if (scanf("%u/%u/%u", &jour, &mois, &annee) != 3) {
        perror("fscanf");
        return EXIT_FAILURE;
    }
    struct date date = { .jour = jour, .mois = mois, .annee = annee };
    printf("Le %u/%u/%u\n", date.jour, date.mois, date.annee);
    return 0;
}

Les champs de bits ne disposent pas d’une adresse et ne peuvent en conséquence se voir appliquer l’opérateur d’adressage. Par ailleurs, nous vous conseillons de ne les employer que pour stocker des nombres non signés, le support des nombres signés n’étant pas garanti par la norme.

Si vous avez poussé la curiosité jusqu’à vérifier la taille de cette structure, il y a de forte chance pour que celle-ci équivaille à celle du type int . En fait, il s’agit de la méthode la plus courante pour conserver les champs de bits : ils sont stockés dans des suites de int . Dès lors, si vous souhaitez économiser de la place, faites en sorte que les données à stocker coïncident le plus possible avec la taille d’un ou plusieurs objets de type int .

Gardez à l’esprit que le compactage de données et les champs de bits répondent à des besoins particuliers et complexifient votre code. Dès lors, ne les utilisez que lorsque cela semble réellement se justifier.

Les drapeaux

Une autre utilisation régulière des opérateurs de manipulation des bits consiste en la gestion des drapeaux. Un drapeau correspond en fait à un bit qui est soit « levé », soit « baissé » dans l’objectif d’indiquer si une situation est vraie ou fausse.

Supposons que nous souhaitions fournir à une fonction un nombre et plusieurs de ses propriétés, par exemple s’il est pair, s’il s’agit d’une puissance de deux et s’il est premier. Nous pourrions bien entendu lui fournir quatre paramètres, mais cela fait finalement beaucoup pour simplement fournir un nombre et, foncièrement, trois bits .

À la place, il nous est possible d’employer un entier dont trois bits seront utilisés pour représenter chaque condition. Par exemple, le premier indiquera si le nombre est pair, le second s’il s’agit d’une puissance de deux et le troisième s’il est premier.

void traitement(int nombre, unsigned char drapeaux)
{
    if (drapeaux & 0x01) /* Si le nombre est pair */
    {
        /* ... */
    }
    if (drapeaux & 0x02) /* Si le nombre est une puissance de deux */
    {
        /*... */
    }
    if (drapeaux & 0x04) /* Si le nombre est premier */
    {
        /*... */
    }
}
int main(void)
{
    int nombre = 2;
    unsigned char drapeaux = 0x01 | 0x02; /* 0000 0011 */
    traitement(nombre, drapeaux);
    nombre = 17;
    drapeaux = 0x04; /* 0000 0100 */
    traitement(nombre, drapeaux);
    return 0;
}

Comme vous le voyez, nous utilisons l’opérateur | pour combiner plusieurs drapeaux et l’opérateur & pour déterminer si un drapeau est levé ou non.

Notez que, chaque drapeau représentant un bit , ceux-ci correspondent toujours à des puissances de deux.

Voilà qui est plus efficace, mais en somme assez peu lisible… En effet, il serait bon de préciser dans notre code à quoi correspond chaque drapeau. Pour ce faire, nous pouvons recourir au préprocesseur afin de clarifier un peu tout cela.

#define PAIR        (1 << 0)
#define PUISSANCE   (1 << 1)
#define PREMIER     (1 << 2)
void traitement(int nombre, unsigned char drapeaux)
{
    if (drapeaux & PAIR) /* Si le nombre est pair */
    {
        /* ... */
    }
    if (drapeaux & PUISSANCE) /* Si le nombre est une puissance de deux */
    {
        /*... */
    }
    if (drapeaux & PREMIER) /* Si le nombre est premier */
    {
        /*... */
    }
}
int main(void)
{
    int nombre = 2;
    unsigned char drapeaux = PAIR | PUISSANCE; /* 0000 0011 */
    traitement(nombre, drapeaux);
    nombre = 17;
    drapeaux = PREMIER; /* 0000 0100 */
    traitement(nombre, drapeaux);
    return 0;
}

Voici qui est mieux.

Pour terminer, remarquez qu’il s’agit d’un bon cas d’utilisation des champs de bits , chacun d’entre eux représentant alors un drapeau.

struct propriete
{
    unsigned pair : 1;
    unsigned puissance : 1;
    unsigned premier : 1;
};
void traitement(int nombre, struct propriete prop)
{
    if (prop.pair) /* Si le nombre est pair */
    {
        /* ... */
    }
    if (prop.puissance) /* Si le nombre est une puissance de deux */
    {
        /*... */
    }
    if (prop.premier) /* Si le nombre est premier */
    {
        /*... */
    }
}
int main(void)
{
    int nombre = 2;
    struct propriete prop = { .pair = 1, .puissance = 1 };
    traitement(nombre, prop);
    return 0;
}

Exercices

Obtenir la valeur maximale d’un type non signé

Maintenant que nous connaissons la représentation des nombres non signés ainsi que les opérateurs de manipulation des bits , vous devriez pouvoir trouver comment obtenir la plus grande valeur représentable par le type unsigned int .

Indice

Afficher/Masquer le contenu masqué

Rappelez-vous : dans la représentation des entiers non signés, chaque bit représente une puissance de deux.

Solution

Afficher/Masquer le contenu masqué
#include <stdio.h>
int main(void)
{
    printf("%u\n", ~0U);
    return 0;
}

Notez bien que nous avons utilisé le suffixe U afin que le type de la constante 0 soit unsigned int et non int (n’hésitez pas à revoir le chapitre relatif aux opérations mathématiques si cela ne vous dit rien).

Cette technique n’est valable que pour les entiers non signés , la représentation où tous les bits sont à un étant potentiellement invalide dans le cas des entiers signés (représentation en complément à un).

Afficher la représentation en base deux d’un entier

Vous le savez, il n’existe pas de format de la fonction printf() qui permet d’afficher la représentation binaire d’un nombre. Pourtant, cela pourrait s’avérer bien pratique dans certains cas, même si la représentation hexadécimale est disponible.

Dans ce second exercice, votre tâche sera de réaliser une fonction capable d’afficher la représentation binaire d’un unsigned int en gros-boutiste .

Indice

Afficher/Masquer le contenu masqué

Pour afficher la représentation gros-boutiste, il va vous falloir commencer par afficher le bit de poids de fort suivit des autres. Pour ce faire, vous allez avoir besoin d’un masque dont seul ce bit sera à un. Pour ce faire, vous pouvez vous aider de l’exercice précédent.

Solution

Résultat (Afficher/Masquer)
#include <stdio.h>
void affiche_bin(unsigned n)
{
    unsigned mask = ~(~0U >> 1);
    unsigned i = 0;
    while (mask > 0)
    {
        if (i != 0 && i % 4 == 0)
            putchar(' ');
        putchar((n & mask) ? '1' : '0');
        mask >>= 1;
        ++i;
    }
    putchar('\n');
}
int main(void)
{
    affiche_bin(1);
    affiche_bin(42);
    return 0;
}

0000 0000 0000 0000 0000 0000 0000 0001
0000 0000 0000 0000 0000 0000 0010 1010

L’expression ~(~0U >> 1) nous permet d’obtenir un masque ou seul le bit de poids fort est à un. Nous pouvons ensuite l’employer successivement en décalant le bit à un de sorte d’obtenir la représentation binaire d’un entier non signé .

À nouveau, faites bien attention que ceci n’est valable que pour les entiers non signés , une représentation dont tous les bits sont à un ou dont seul le bit de poids fort est à un étant possiblement incorrecte dans le cas des entiers signés.

Déterminer si un nombre est une puissance de deux

Vous le savez : les puissances de deux ont la particularité de n’avoir qu’un seul bit à un, tous les autres étant à zéro. Toutefois, elles ont une autre propriété : si l’on soustrait un à une puissance de deux n n n n , tous les bits précédent celui de la puissance seront mis à un (par exemple 0000 1000 - 1 == 0000 0111 ). En particulier, on remarque que n n n n et n − 1 n − 1 n-1 n − 1 n’ont aucun bit à 1 en commun. Réciproquement, si n n n n n’est pas une puissance de 2, alors le bit à 1 le plus fort est aussi à 1 dans n − 1 n − 1 n-1 n − 1 . par exemple 0000 1010 - 1 == 0000 1001 ).

Sachant cela, il nous est possible de créer une fonction très simple déterminant si un nombre est une puissance de 2 ou non.

Résultat (Afficher/Masquer)
#include <stdio.h>
int puissance_de_deux(unsigned int n)
{
    return n != 0 && (n & (n - 1)) == 0;
}
int main(void)
{
    if (puissance_de_deux(256))
        printf("256 est une puissance de deux\n");
    else
        printf("256 n'est pas une puissance de deux\n");
    if (puissance_de_deux(48))
        printf("48 est une puissance de deux\n");
    else
        printf("48 n'est pas une puissance de deux\n");
    return 0;
}

256 est une puissance de deux
48 n'est pas une puissance de deux
En résumé
  1. Le C fourni six opérateurs de manipulation des bits ;
  2. Ces derniers travaillant directement sur la représentation des entiers, il est impératif d’éviter d’obtenir des représentations potentiellement invalide dans le cas des entiers signés ;
  3. L’utilisation de masques permet de sélectionner ou modifier une portion d’un nombre entier ;
  4. Les champs de bits permettent de stocker des entiers de taille arbitraire, mais doivent toujours avoir une taille inférieure à celle du type int . Par ailleurs, ils n’ont pas d’adresse et ne supportent pas forcément le stockage d’entiers signés ;
  5. Les drapeaux constituent une solution élégante pour stocker des états binaires (« vrai » ou « faux »).


Internationalisation et localisation - Le langage C • Tutoriels • Zeste de Savoir

Internationalisation et localisation

Programmation et algorithmique
  • Manipulation des bits
  • Sommaire
  • La représentation des chaînes de caractères

Cela est resté finalement assez discret jusqu’ici, mais en y regardant de plus près, les programmes que nous avons réalisés sont en fait destinés à un environnement anglophone. En effet, prenez par exemple les entrées : si nous souhaitons fournir un nombre flottant à notre programme, nous devons utiliser le point comme séparateur entre la partie entière et décimale. Or, dans certains pays, on pourrait vouloir utiliser la virgule à la place. Cela nous paraît moins étrange étant donné que les constantes flottantes sont écrites de cette manière en C, mais il n’en va pas de même pour nos utilisateurs.

Ce qu’il faudrait finalement, c’est que nos programmes puissent s’adapter aux usages, coutumes et langues de notre utilisateur et c’est que nous allons voir (partiellement) dans ce chapitre. :)

  • Définitions
  • La fonction setlocale
  • La catégorie LC_NUMERIC
  • La catégorie LC_TIME

Définitions

Avant toute chose, il nous est nécessaire de définir deux concepts afin de bien cerner de quoi nous allons parler.

L’internationalisation

L'internationalisation (parfois abrégée « i18n ») est un procédé par lequel un programme est rendu capable de s’adapter aux préférences linguistiques et régionales d’un utilisateur.

La localisation

La localisation (parfois abrégée « l10n ») est une opération par laquelle un programme internationalisé se voit fournir les informations nécessaires pour s’adapter aux préférences linguistiques et régionales d’un utilisateur.

La fonction setlocale

De manière générale, les programmes que nous avons conçus jusqu’ici étaient déjà partiellement internationalisés, car la bibliothèque standard du langage C l’est dans une certaine mesure. Toutefois, nous n’avons jamais recouru à un processus de localisation pour que ceux-ci s’adaptent à nos usages. Nous vous le donnons en mille : la localisation en C s’effectue à l’aide de… la fonction setlocale() .

char *setlocale(int categorie, char *localisation);

Cette fonction attends deux arguments : une catégorie et la localisation qui doit être employée pour cette catégorie. Elle retourne la localisation demandée si elle a pu être appliquée, un pointeur nul sinon.

Les catégories

La bibliothèque standard du C divise la localisation en plusieurs catégories, plus précisément cinq :

  1. La catégorie LC_COLLATE qui modifie le comportement des fonctions strcoll() et strxfrm() ;
  2. La catégorie LC_CTYPE qui adapte le comportement des fonctions de traduction entre chaîne de caractères et chaînes de caractères larges (nous y viendrons bientôt), ainsi que les fonctions de l’en-tête <ctype.h> ;
  3. La catégorie LC_MONETARY qui influence le comportement de la fonction localeconv() ;
  4. La catégorie LC_NUMERIC qui altère le comportement des fonctions *printf() et *scanf() ainsi que des fonctions de conversions de chaînes de caractères en ce qui concerne les nombres flottants ;
  5. La catégorie LC_TIME qui change le comportement de la fonction strftime() .

Enfin, la catégorie LC_ALL (qui n’en est pas vraiment une) représente toutes les catégories en même temps.

Nous ne nous attarderons que sur les catégories LC_NUMERIC et LC_TIME dans la suite de ce chapitre.

Les localisations

La bibliothèque standard prévoit deux localisations possibles :

  1. La localisation "C" qui correspond à celle par défaut. Celle-ci utilise les usages anglophones ;
  2. La localisation "" (une chaîne vide) qui correspond à celle utilisée par votre système.

Il est également possible de fournir un pointeur nul comme localisation, auquel cas la localisation actuelle est retournée.

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char *s;
    s = setlocale(LC_ALL, NULL);
    puts(s);
    if (setlocale(LC_ALL, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    s = setlocale(LC_ALL, NULL);
    puts(s);
    return 0;
}
Résultat
C
fr_BE.UTF-8

Comme vous le voyez, la localisation de départ est bien C .

La forme que prend la localisation dépend de votre système. Sous unixoïdes et dans notre exemple, elle prend la forme de la langue en minuscule (au format ISO 639) suivie d’un tiret bas et du pays en majuscule (au format ISO 3166–1) et, éventuellement, terminée par un point et par l’encodage utilisé.

La catégorie LC_NUMERIC

La catégorie LC_NUMERIC permet de modifier le comportement des fonctions scanf() et printf() afin qu’elles adaptent leur gestion et leur affichage des nombres flottants.

La catégorie LC_NUMERIC affectent également les fonctions atoi() , atol() , atoll() , strtol() , strtoll() , strtoul() , strtoull() , strtoimax() , strtoumax() , atof() , strtof() , strtod() et strtold() qui forment une suite de fonctions dédiées à la conversion de chaînes de caractères vers des nombres. Toutefois, nous ne nous étendrons pas sur ces dernières.

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    double f;
    if (setlocale(LC_NUMERIC, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    printf("Veuillez entrer un nombre flottant : ");
    if (scanf("%lf", &f) != 1)
    {
        perror("scanf");
        return EXIT_FAILURE;
    }
    printf("Vous avez entré : %f.\n", f);
    return 0;
}
Résultat (localisation francophone)
Veuillez entrer un nombre flottant : 45,5
Vous avez entré : 45,500000.
Veuillez entrer un nombre flottant : 45.5
Vous avez entré : 45,000000.

Comme vous le voyez, avec une locale francophone et après l’appel à setlocale() , seule la virgule est considérée comme séparateur de la partie entière et de la partie décimale.

La catégorie LC_TIME

La catégorie LC_TIME modifie le comportement de la fonction strftime() .

size_t strftime(char *chaine, size_t taille, char *format, struct tm *date);

Cette fonction, déclarée dans l’en-tête <time.h> , écrit dans la chaîne de caractères chaine différents éléments décrivant la date date en suivant la chaîne de format format (à l’image de la fonction snprintf() ). S’il y a assez de place dans la chaîne chaine pour écrire l’entièreté des données, la fonction retourne le nombre de caractères écrits, sinon elle retourne zéro.

Structure tm

La date doit être sous la forme d’une structure tm (déclarée dans l’en-tête <time.h> ), structure qui peut être obtenue via la fonction localtime() .

struct tm *localtime(time_t *date);

Cette fonction attend simplement l’adresse d’un objet de type time_t et retourne l’adresse d’une structure tm .

La fonction strftime() supporte un grand nombre d’indicateurs de conversion pour construire la chaîne de format. En voici une liste non exhaustive.

Indicateur de conversion | Signification
A | Nom du jour de la semaine
B | Nom du mois de l’année
d | Jour du mois
Y | Année sur quatre chiffres
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int
main(void)
{
    char buf[255];
    time_t t;
    if (setlocale(LC_TIME, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    if (time(&t) == (time_t)-1)
    {
        perror("time");
        return EXIT_FAILURE;
    }
    struct tm *tm = localtime(&t);
    if (tm == NULL)
    {
        perror("localtime");
        return EXIT_FAILURE;
    }
    if (!strftime(buf, sizeof buf, "%A %d %B %Y", tm))
    {
        perror("strftime");
        return EXIT_FAILURE;
    }
    printf("%s\n", buf);
    return 0;
}
Résultat
Wednesday 07 November 2018
mercredi 07 novembre 2018

À nouveau, comme vous pouvez le voir, suivant la locale utilisée la chaîne produite n’est pas la même.


La représentation des chaînes de caractères - Le langage C • Tutoriels • Zeste de Savoir

La représentation des chaînes de caractères

Les séquences d'échappement

Avant de nous pencher sur la représentation des chaînes de caractères, un petit aparté sur les séquences d’échappement s’impose. Vous avez vu tout au long de ce tutoriel la séquence \0 , utilisée pour représenter le caractère nul terminant une chaîne de caractères, sans qu’elle ne vous soit véritablement expliquée.

Cette séquence est en fait tout simplement remplacée par la valeur octale indiquée après le symbole \ , ici zéro. Ainsi les trois définitions ci-dessous sont équivalentes.

char s1[] = { 0, 0 };
char s2[] = { '\0', '\0' };
char s3[] = "\0"; /* N'oubliez pas qu'un '\0' est ajouté automatiquement s'agissant d'une chaîne de caractères */

Ces séquences peuvent toutefois être utilisées pour produire n’importe quelle valeur, du moment qu’elle ne dépasse pas la capacité du type char . Ainsi, la séquence \42 a pour valeur 42.

L’octal n’étant cependant pas la base la plus pratique à utiliser, il est également possible d’employer la base hexadécimale en faisant suivre le symbole \ de la lettre « x ». Ainsi, la séquence \x0 est équivalente à la séquence \0 .

Les tables de correspondances

Cette section ne constitue qu’une introduction posant les bases nécessaires pour la suite. Si vous souhaitez en apprendre davantage à ce sujet, nous vous invitons à lire le cours de Maëlan sur les encodages sur lequel nous nous sommes appuyé pour écrire cette section.

La table ASCII

Vous le savez, un ordinateur ne manipule jamais que des nombres et, de ce fait, toute donnée non numérique à la base doit être numérisée pour qu’il puisse la traiter. Les caractères ne font dès lors pas exception à cette règle et il a donc été nécessaire de trouver une solution pour transformer les caractères en nombres.

La solution retenue dans leur cas a été d’utiliser des tables de correspondance (aussi appelées code page en anglais) entre nombres et caractères. Chaque caractère se voit alors attribuer un nombre entier qui est ensuite stocké dans un char .

Vocabulaire

Le terme de « jeu de caractères » ( character set en anglais, ou charset en abrégé) est également souvent employé. Ce dernier désigne tout simplement un ensemble de caractères. Ainsi, l’ASCII est également un jeu de caractères qui comprend les caractères composant sa table de correspondance.

Une des tables les plus connues est la table ASCII. Cette table sert quasi toujours de base à toutes les autres tables existantes, ce que vous pouvez d’ailleurs vérifier aisément.

#include <stdio.h>
int
main(void)
{
    char ascii[] = "ASCII";
    for (unsigned i = 0; ascii[i] != '\0'; ++i)
        printf("'%c' = %d\n", ascii[i], ascii[i]);
    return 0;
}
Résultat
'A' = 65
'S' = 83
'C' = 67
'I' = 73
'I' = 73

Si vous jetez un coup d’œil à la table vous verrez qu’au caractère A correspond bien le nombre 65 (en décimal), au caractère S le nombre 83, au caractère C le nombre 67 et au caractère I le nombre 73. Le compilateur a donc remplacé pour nous chaque caractère par le nombre entier correspondant dans la table ASCII et a ensuite stocké chacun de ces nombres dans un char de la chaîne de caractères ascii .

Toutefois, si vous regardez plus attentivement la table ASCII, vous remarquerez qu’il n’y a pas beaucoup de caractères. En fait, il y a de quoi écrire en anglais et… en latin. :-°

La raison est historique : l’informatique s’étant principalement développé aux États-Unis, la table choisie ne nécessitait que de quoi écrire l’anglais (l’acronyme ASCII signifie d’ailleurs American Standard Code for Information Interchange ). Par ailleurs, la capacité des ordinateurs de l’époque étant limitée, il était le bienvenu que tous ces nombres puissent tenir sur un seul multiplet (il n’était pas rare à l’époque d’avoir des machines dont les multiplets comptaient 7 bits ).

La multiplication des tables

L’informatique s’exportant, il devenait nécessaire de pouvoir employer d’autres caractères. C’est ainsi que d’autres tables ont commencés à faire leur apparition, quasiment toutes basées sur la table ASCII et l’étendant comme le latin-1 ou ISO 8859–1 pour les pays francophones et plus généralement « latins », l' ISO 8859–5 pour l’alpabet cyrillique ou l' ISO 8859–6 pour l’alphabet arabe.

Ainsi, il devenait possible d’employer d’autres caractères que ceux de la table ASCII sans modifier la représentation des chaînes de caractères. En effet, en créant grosso modo une table par alphabet, il était possible de maintenir la taille de ces tables dans les limites du type char .

Cependant, cette multiplicité de tables de correspondance amène deux problèmes :

  1. Il est impossible de mélanger des caractères de différents alphabets puisqu’une seule table de correspondance peut être utilisée.
  2. L’échange de données peut vite devenir problématique si le destinateur et la destinataire n’utilisent pas la même table de correspondance.

Par ailleurs, si cette solution fonctionne pour la plupart des alphabets, ce n’est par exemple pas le cas pour les langues asiatiques qui comportent un grand nombre de caractères.

La table Unicode

Afin de résoudre ces problèmes, il a été décidé de construire une table unique, universelle qui contiendrait l’ensemble des alphabets : la table Unicode. Ainsi, finis les problèmes de dissonance entre tables : à chaque caractère correspond un seul et unique nombre. Par ailleurs, comme tous les caractères sont présents dans cette table, il devient possible d’employer plusieurs alphabets. Il s’agit aujourd’hui de la table la plus utilisée.

Sauf qu’avec ses 137.374 caractères (à l’heure où ces lignes sont écrites), il est impossible de représenter les chaînes de caractères simplement en assignant la valeur entière correspondant à chaque caractère dans un char . Il a donc fallu changer de tactique avec l’Unicode en introduisant des méthodes pour représenter ces valeurs sur plusieurs char au lieu d’un seul. Ces représentations sont appelées des encodages.

Il existe trois encodages majeures pour l’Unicode : l’UTF-32, l’UTF-16 et l’UTF-8.

L’UTF-32

L’UTF-32 stocke la valeur correspondant à chaque caractère sur 4 char en utilisant la même représentation que les entiers non signés. Autrement dit, les 4 char sont employés comme s’ils constituaient les 4 multiplets d’un entier non signé. Ainsi, le caractère é qui a pour valeur 233 (soit E9 en hexadécimal) donnera la suite \xE9\x0\x0\x0 (ou l’inverse suivant le boutisme utilisé) en UTF-32.

Oui, vous avez bien lu, vu qu’il emploie la représentation des entiers non signés, cet encodage induit l’ajout de pas mal de char nuls. Or, ces derniers étant utilisés en C pour représenter la fin d’une chaîne de caractères, il est assez peu adapté… De plus, cette représentation implique de devoir gérer les questions de boutisme.

L’UTF-16

L’UTF-16 est fort semblable à l’UTF-32 mis à part qu’il stocke la valeur correspondant à chaque caractère sur 2 char au lieu de 4. Avec une différence notable toutefois : les valeurs supérieures à 65535 (qui sont donc trop grandes pour être stockées sur deux char ) sont, elles, stockées sur des paires de 2 char .

Toutefois, dans notre cas, l’UTF-16 pose les mêmes problèmes que l’UTF-32 : il induit la présence de char nuls au sein des chaînes de caractères et nécessite de s’occuper des questions de boutisme.

L’UTF-8

L’UTF-8 utilise un nombre variable de char pour stocker la valeur d’un caractère. Plus précisément, les caractères ASCII sont stockés sur un char , comme si la table ASCII était utilisée, mais tous les autres caractères sont stockés sur des séquences de deux ou plusieurs char .

À la différence de l’UTF-32 ou de l’UTF-16, cet encodage ne pose pas la question du boutisme (s’agissant de séquences d’un ou plusieurs char ) et n’induit pas l’ajout de caractères nuls (la méthode utilisée pour traduire la valeur d’un caractère en une suite de char a été pensée à cet effet).

Par exemple, en UTF-8, le caractère é sera traduit par la suite de char \xc3\xa9 . Vous pouvez vous en convaincre en exécutant le code suivant.

#include <stdio.h>
int
main(void)
{
    char utf8[] = u8"é";
    for (unsigned i = 0; utf8[i] != '\0'; ++i)
        printf("%02x\n", (unsigned char)utf8[i]);
    return 0;
}
Résultat
c3
a9

Notez que nous avons fait précédé la chaîne de caractères "é" de la suite u8 afin que celle-ci soit encodée en UTF-8.

Comme vous le voyez, notre chaîne est bien composée de deux char ayant pour valeur \xc3 et \xa9 . Toutfois, si l’UTF-8 permet de stocker n’importe quel caractère Unicode au sein d’une chaîne de caractères, il amène un autre problème : il n’y a plus de correspondance systématique entre un char et un caractère .

En effet, si vous prenez l’exemple présenté juste au-dessus, nous avons une chaîne composée d’un seul caractère, é , mais dans les faits composée de deux char valant \xc3 et \xa9 . Or, ni \xc3 , ni \xa9 ne représentent un caractère (rappelez-vous : seuls les caractères ASCII tiennent sur un char en UTF-8), mais uniquement leur succession.

Ceci amène plusieurs soucis, notamment au niveau de la manipulation des chaînes de caractères, puisque les fonctions de la bibliothèque standard partent du principe qu’à un char correspond un caractère (pensez à strlen() , strchr() ou strpbrk() ).

Des chaînes, des encodages

Bien, nous savons à présent comment les chaînes de caractère sont représentées. Toutefois, il reste une question en suspend : vu qu’il existe pléthore de tables de correspondance, quelle est celle qui est utilisée en C ? Autrement dit : quelle table est utilisée en C pour représenter les chaînes de caractères ? Eh bien, cela dépend de quelles chaînes de caractères vous parlez… :-°

Pour faire bref :

  1. La table de correspondance et l’encodage des chaînes littérales dépendent de votre compilateur ;
  2. La table de correspondance et l’encodage des chaînes reçues depuis le terminal dépendent de votre système d’exploitation ;
  3. La table de correspondance et l’encodage des fichiers dépendent de ceux utilisés lors de leur écriture (typiquement celui utilisé par un éditeur de texte).

Et le plus amusant, c’est qu’ils peuvent tous être différents !

L’exemple suivant illustre tout l’enfer qui peut se cacher derrière cette possibilité.

#include <stdio.h>
#include <stdlib.h>
static void chomp(char *s)
{
    while (*s != '\n' && *s != '\0')
        ++s;
    *s = '\0';
}
int
main(void)
{
    char ligne[255];
    printf("Cette ligne est encodée en UTF-8\n");
    if (fgets(ligne, sizeof ligne, stdin) == NULL)
    {
        perror("scanf");
        return EXIT_FAILURE;
    }
    chomp(ligne);
    printf("Cette chaîne en ISO 8859-1 : %s\n", ligne);
    FILE *fp = fopen("fichier.txt", "r");
    if (fp == NULL)
    {
        perror("fopen");
        return EXIT_FAILURE;
    }
    if (fgets(ligne, sizeof ligne, fp) == NULL)
    {
        perror("fscanf");
        return EXIT_FAILURE;
    }
    chomp(ligne);
    printf("Et celle-là en IBM 850 : %s\n", ligne);
    fclose(fp);
    return 0;
}
Résultat
Cette ligne est encodÃ©e en UTF-8
L'ISO 8859-1 c'est tellement plus élégant !
Cette chaÃ®ne en ISO 8859-1 : L'ISO 8859-1 c'est tellement plus élégant !
Et celle-lÃ  en IBM 850 : Moi aussi je suis lgant !

Cet exemple illustre l’affichage d’un programme dont les chaîne littérales sont encodées en UTF-8, les entrées du terminal en ISO 8859–1 et le texte d’un fichier (dont le contenu est « Moi aussi je suis élégant ! ») en IBM 850.

Que se passe-t-il, donc ?

  1. Tout d’abord, la chaîne « Cette ligne est encodée en UTF-8 » est affichée sur le terminal. Comme celui-ci utilise l’ISO 8859–1 et non l’UTF-8, la suite \xc3\xa9 (le « é » en UTF-8) est interprétée comme deux caractères distincts (l’ISO 8859–1 encode les caractères sur un seul multiplet) : « Ã » et « © » (dont les valeurs correspondent à \xc3 et \xa9 dans la table ISO 8859–1).
  2. Ensuite, la chaîne « L’ISO 8859–1 c’est tellement plus élégant ! » est récupérée en entrée et est affichée. Étant donné qu’elle est lue depuis le terminal, elle est encodée en ISO 8859–1 et est donc correctement affichée. Notez que comme « Cette chaîne en ISO 8859–1 : » est une chaîne littérale, elle est encodée en UTF-8, d’où la suite de caractères « Ã® » au lieu du « î » ( \xc3\xae en UTF-8).
  3. Enfin, la chaîne « Moi aussi je suis élégant ! » est lue depuis le fichier fichier.txt . Celle-ci est encodée en IBM 850 qui associe au caractère « é » le nombre 130 ( \x82 ). Cependant, ce nombre ne correspond à rien dans la table ISO 8859–1, du coup, nous obtenons « Moi aussi je suis lgant ! ». En passant, remarquez à nouveau que la chaîne littérale « Et celle-là en IBM 850 : » est elle encodée en UTF-8, ce qui fait que la suite correspondant au caractère « à » ( \xc3\xa0 ) est affichée comme deux caractères : « Ã » et un espace insécable.

Vers une utilisation globale de l’UTF-8

Toutefois, si ce genre de situations peut encore se présenter, de nos jours, elles sont rares car la plupart des compilateurs (c’est le cas de Clang et GCC par exemple) et systèmes d’exploitation (à l’exception notable de Windows, mais vous évitez ce problème en utilisant MSys2) utilisent l’Unicode comme table de correspondance et l’UTF-8 comme encodage par défaut. Cependant, rien n’est imposé par la norme de ce côté , il est donc nécessaire de vous en référer à votre compilateur et/ou à votre système d’exploitation sur ce point.

Choisir la table et l’encodage utilisés par le compilateur

Cela étant dit, dans le cas du compilateur, comme nous l’avons déjà vu, il est possible d’imposer l’UTF-8 comme encodage en faisant précédé vos chaînes de caractères par la suite u8 . Également, certains compilateurs vous permettent de spécifier l’encodage que vous souhaitez utiliser, c’est par exemple le cas de GCC qui fourni l’option --fexec-charset=<encodage> à cet effet.

#include <stdio.h>
int
main(void)
{
    char s[] = "Élégant";
    for (unsigned i = 0; s[i] != '\0'; ++i)
        printf("%02x ", (unsigned char)s[i]);
    printf("\n");
    return 0;
}
Résultat
$ zcc main.c --fexec-charset=ISO8859-1 -o x
$ ./x
c9 6c e9 67 61 6e 74
$ zcc main.c -o x
$ ./x
c3 89 6c c3 a9 67 61 6e 74

Comme vous le voyez, nous avons demandé à GCC d’utiliser l’encodage ISO 8859–1, puis nous n’avons rien spécifié et c’est alors l’UTF-8 qui a été utilisé.

En résumé
  1. Les séquences d’échappement \n et \xn sont remplacées, respectivement, par la valeur octale ou hexadécimale précisée après \ ou \x .
  2. Les caractères sont numérisés à l’aide de tables de correspondance comme la table ASCII ou la table ISO 8859–1 ;
  3. L’Unicode a pour vocation de construire une table de correspondance universelle pour tous les caractères ;
  4. Une chaîne de caractères étant une suite de char finie par un multiplet nul, il est nécessaire de répartir les valeurs supérieures à la capacité d’un char sur plusieurs char suivant un encodage comme l’UTF-8 ;
  5. La table de correspondance et l’encodage utilisés pour représenter les chaîne de caractères littérales dépendent du compilateur ;
  6. La table de correspondance et l’encodage utilisés pour représenter les chaîne de caractères lues depuis le terminal dépendent de votre système ;
  7. La table de correspondance et l’encodage des fichiers dépendent de ceux utilisés lors de leur écriture ;
  8. Il est possible d’imposer l’UTF-8 comme encodage pour une chaîne de caractères en la faisant précéder de la suite u8 ;
  9. L’option --fexec-charset de GCC permet de préciser l’encodage désiré pour représenter les chaînes de caractères.


Les caractères larges - Le langage C • Tutoriels • Zeste de Savoir

Les caractères larges

Introduction

Si la non correspondance entre un char et un caractère n’est pas forcément gênante (elle n’empêche pas de lire ou d’écrire du texte par exemple), elle peut l’être dans d’autres cas, par exemple lorsque l’on souhaite compter le nombre de caractères composant une chaîne de caractères.

Pour ces derniers cas, il faudrait revenir à une correspondance une à une. Or, vous le savez, pour cela il faudrait un autre type que le type char , ce dernier n’ayant pas une taille suffisante (d’où le recours aux encodages comme l’UTF-8).

Dans cette optique, le type wchar_t (pour wide character , littéralement « caractère large »), défini dans l’en-tête <stddef.h> , a été introduit. Celui-ci n’est rien d’autre qu’un type entier (signé ou non signé) capable de représenter la valeur la plus élevée d’une table de correspondance donnée. Ainsi, il devient par exemple possible de stocker toutes les valeurs de la table Unicode sur un système utilisant celle-ci.

Toutefois, disposer d’un type dédié n’est pas suffisant. En effet, il nous faut également « décoder » nos chaînes de caractères afin de récupérer les valeurs correspondantes à chaque caractère (qui, pour rappel, sont pour l’instant potentiellement réparties sur plusieurs char ). Cependant, pour effectuer ce décodage, il est nécessaire de connaître l’encodage utilisé pour représenter les chaînes de caractères. Or, vous le savez, cet encodage est susceptible de varier suivant qu’il s’agit d’une chaîne littérale, d’une entrée récupérée depuis le terminal ou de données provenant d’un fichier.

Dès lors, assez logiquement, la méthode pour convertir une chaîne de caractères en une chaîne de caractères larges varie suivant le même critère.

Traduction en chaîne de caractères larges et vice versa

Les chaînes de caractères littérales

Le cas des chaînes de caractères littérales est le plus simple : étant donné que la table et l’encodage sont déterminés par le compilateur, c’est lui qui se charge également de leur conversion en chaînes de caractères larges.

Pour demander au compilateur de convertir une chaîne de caractères littérale en une chaîne de caractères larges littérale, il suffit de précéder la définition d’une chaîne de caractères littérale de la lettre L .

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    wchar_t ws[] = L"é";
    printf("%u\n", (unsigned)ws[0]);
    return 0;
}
Résultat
233

Comme vous le voyez, nous obtenons 233, qui est bien la valeur du caractère é dans la table Unicode. Techniquement, le compilateur a lu les deux char composant le caractère é et en a recalculé la valeur : 233, qu’il a ensuite assigné au premier élément du tableau ws .

Notez qu’il est également possible de faire de même pour un caractère littéral seul, en le précédent également de la lettre L . Le code suivant produit à nouveau le même résultat.

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    printf("%u\n", (unsigned)L'é');
    return 0;
}
Résultat
233

Les entrées récupérées depuis le terminal

Dans le cas des chaînes de caractères lues depuis le terminal, la conversion doit être effectuée par nos soins. Pour ce faire, la bibliothèque standard nous fournis deux fonctions déclarées dans l’en-tête <stdlib.h> : mbstowcs() et wcstombs() .

La fonction mbstowcs

size_t mbstowcs(wchar_t *destination, char *source, size_t max);

La fonction mbstowcs() traduit la chaîne de caractères source en une chaîne caractères larges d’au maximum max caractères qui sera stockée dans destination . Elle retourne le nombre de caractères larges écrits, sans compter le caractère nul final. En cas d’erreur, la fonction renvoie (size_t)-1 (soit la valeur maximale du type size_t ).

Le code ci-dessous récupère une ligne depuis le terminal, la converti en une chaîne de caractères larges et affiche ensuite la valeur de chaque caractère la composant.

#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char ligne[255];
    if (fgets(ligne, sizeof ligne, stdin) == NULL)
    {
        perror("fgets");
        return EXIT_FAILURE;
    }
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    wchar_t ws[sizeof ligne];
    if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
    {
        perror("mbstowcs");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; ws[i] != L'\n' && ws[i] != L'\0'; ++i)
        printf("%d ", ws[i]);
    printf("\n");
    return 0;
}
Résultat
Élégant
201 108 233 103 97 110 116

Comme vous le voyez, nous obtenons 201 et 233 pour « É » et « é », qui correspondent bien aux valeurs que leur attribue la table Unicode.

La catégorie LC_CTYPE

Vous l’aurez sûrement remarqué : la fonction setlocale() est employée dans l’exemple précédent pour modifier la localisation de la catégorie LC_CTYPE . Cette catégorie affecte le comportement des fonctions de traduction entre chaînes de caractères et chaînes de caractères larges.

En fait, l’appel à setlocale() permet aux fonctions de traductions de connaître la table et l’encodage utilisés par notre système et ainsi de traduire correctement les entrées récupérées depuis le terminal.

Localisation par défaut

Avec la localisation par défaut ( C ), les fonctions de traductions se contentent d’affecter la valeur de chaque char à un wchar_t ou inversement.

La fonction wcstombs

size_t wcstombs(char *destination, wchar_t *source, size_t max);

La fonction wcstombs() effectue l’opération inverse de la fonction mbstowcs() : elle traduit la chaîne de caractères larges source en une chaîne de caractères d’au maximum max caractères qui sera stockée dans destination .

La fonction retourne le nombre de multiplets écrits ou (size_t)-1 en cas d’erreur.

#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    char ligne[255];
    if (fgets(ligne, sizeof ligne, stdin) == NULL)
    {
        perror("fgets");
        return EXIT_FAILURE;
    }
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    wchar_t ws[sizeof ligne];
    if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
    {
        perror("mbstowcs");
        return EXIT_FAILURE;
    }
    size_t n = wcstombs(ligne, ws, sizeof ligne);
    if (n == (size_t)-1)
    {
        perror("wcstombs");
        return EXIT_FAILURE;
    }
    printf("%zu multiplet(s) écrit(s) : %s\n", n, ligne);
    return 0;
}
Résultat
Élégant
10 multiplet(s) écrit(s) : Élégant

Les données provenant d’un fichier

Dans le cas des fichiers, malheureusement, il n’y a pas de solution pour déterminer la table de correspondance ou l’encodage utilisés lors de l’écriture de ses données. La seule solution est de s’assurer d’une manière ou d’une autre que ces derniers sont les mêmes que ceux employés par le système.

Des chaînes, des encodages

Ceci étant posé, nous nous permettons d’attirer votre attention sur un point : étant donné que la table et l’encodage utilisé pour représenter les chaînes de caractères littérales et les entrées du terminal sont susceptibles d’être différents, il est parfaitement possible d’obtenir des chaînes de caractères larges différentes.

Ainsi, si le code suivant donnera le plus souvent le résultat attendu étant donné que l’emploie de l’UTF-8 se généralise, rien ne vous le garanti de manière générale. En effet, si, par exemple, les chaînes littérales sont encodées en UTF-8 et les entrées du terminal en IBM 850, alors la fonction nombre_de_e() ne retournera pas le bon résultat.

#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
static void chomp(char *s)
{
    while (*s != '\n' && *s != '\0')
        ++s;
    *s = '\0';
}
static unsigned nombre_de_e(wchar_t *ws)
{
    unsigned n = 0;
    while (*ws != L'\n' && *ws != L'\0')
    {
        switch(*ws)
        {
        case L'é':
        case L'ê':
        case L'è':
        case L'e':
        case L'É':
        case L'Ê':
        case L'È':
        case L'E':
            ++n;
            break;
        }
        ++ws;
    }
    return n;
}
int main(void)
{
    char ligne[255];
    if (fgets(ligne, sizeof ligne, stdin) == NULL)
    {
        perror("fgets");
        return EXIT_FAILURE;
    }
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    chomp(ligne);
    wchar_t ws[sizeof ligne];
    if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
    {
        perror("mbstowcs");
        return EXIT_FAILURE;
    }
    printf("Il y a %zu \"e\" dans la phrase : \"%s\"\n", nombre_de_e(ws), ligne);
    return 0;
}
Résultat
Élégamment trouvé !
Il y a 4 "e" dans la phrase : "Élégamment trouvé !"

L'en-tête <wchar.h>

Nous venons de le voir, il nous est possible de convertir une chaîne de caractères en chaîne de caractères larges et vice versa à l’aide des fonctions mbstowcs() et wcstombs() . Toutefois, c’est d’une part assez fastidieux et, d’autre part, nous ne disposons d’aucune fonction pour manipuler ces chaînes de caractères larges (pensez à strlen() , strcmp() ou strchr() ).

Heureusement pour nous, la bibliothèque standard nous fournis un nouvel en-tête : <wchar.h> pour nous aider. :)
Ce dernier fournit à peu de chose près un équivalent gérant les caractères larges de chaque fonction de lecture/écriture et de manipulation des chaînes de caractères.

<wchar.h> : les fonctions de lecture/écriture

Le tableau ci-dessous liste les fonctions de lecture/écriture classique et leur équivalent gérant les caractères larges.

<stdio.h> | <wchar.h>
printf | wprintf
fprintf | fwprintf
scanf | wscanf
fscanf | fwscanf
getc | getwc
fgetc | fgetwc
getchar | getwchar
fgets | fgetws
putc | putwc
fputc | fputwc
putchar | putwchar
fputs | fputws
ungetc | ungetwc

Les fonctions de lecture/écriture « larges » s’utilisent de la même manière que les autres, la seule différence est qu’elles effectuent les conversions nécessaires pour nous. Ainsi, l’exemple suivant est identique à celui de la section précédente, si ce n’est que la fonction fgetws() se charge d’effectuer la conversion en chaîne de caractères larges pour nous.

#include <locale.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
int main(void)
{
    wchar_t ligne[255];
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    if (fgetws(ligne, sizeof ligne / sizeof ligne[0], stdin) == NULL)
    {
        perror("fgetws");
        return EXIT_FAILURE;
    }
    for (unsigned i = 0; ligne[i] != L'\n' && ligne[i] != L'\0'; ++i)
        printf("%d ", ligne[i]);
    printf("\n");
    return 0;
}
Résultat
Élégant
201 108 233 103 97 110 116

N’oubliez pas de faire appel à la fonction setlocale() avant toute utilisation des foctions de lecture/écriture larges !

L’orientation des flux

Toutefois, les fonctions de lecture/écriture larges amènent une subtilité supplémentaire. En effet, lorsque nous vous avons présenté les flux et les fonctions de lecture/écriture, nous avons omis de vous parler d’une caractéristique des flux : leur orientation.

L’orientation d’un flux détermine si ce dernier manipule des caractères larges ou des caractères simples. Lors de sa création, un flux n’a pas d’orientation, c’est la première fonction de lecture/écriture utilisée sur ce dernier qui la déterminera. Si c’est une fonction de lecture/écriture classique, le flux sera dit « orienté multiplets », si c’est une fonction de lecture/écriture large, le flux sera dit « orienté caractères larges ».

Cette orientation a son importance et amène quatre restrictions supplémentaires quant à l’usage des flux.

  1. Une fois l’orientation d’un flux établie, elle ne peut plus être changée ;

  2. Un flux orienté multiplets ne peut pas être utilisé par des fonctions de lecture/écriture larges et, inversement, un flux orienté caractères larges ne peut pas être utilisé par des fonctions de lecture/écriture classiques ;

fgets(s, sizeof s, stdin);
/* Faux, car stdin est déjà un flux orienté multiplets. */
fgetws(ws, sizeof ligne / sizeof ligne[0], stdin);

  3. Un flux binaire orienté caractères larges voit ses déplacements possibles via la fonction fseek() davantage limités. Ainsi, seul le repère SEEK_SET avec une valeur nulle ou précédemment retournée par la fonction ftell et le répère SEEK_CUR avec une valeur nulle peuvent être utilisés ;

  4. Si la position au sein d’un flux orienté caractères larges est modifée et est suivie d’une opération d’écriture, alors le contenu qui suit les données écrites devient indéterminé, sauf si le déplacement a lieu à la fin du fichier (c’est assez logique, puisque dans ce cas il n’y a rien après).

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
int main(void)
{
    wchar_t ligne[255];
    FILE *fp = fopen("fichier.txt", "w+");
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    if (fputws(L"Une ligne\n", fp) < 0)
    {
        perror("fputws");
        return EXIT_FAILURE;
    }
    if (fputws(L"Une autre ligne\n", fp) < 0)
    {
        perror("fputws");
        return EXIT_FAILURE;
    }
    rewind(fp);
    /*
     * On réécrit par dessus « Une ligne ».
     */
    if (fputws(L"Une ligne\n", fp) < 0)
    {
        perror("fputws");
        return EXIT_FAILURE;
    }
    /*
     * Obligatoire entre une opération d'écriture et une opération de lecture, rappelez-vous.
     */
    if (fseek(fp, 0L, SEEK_CUR) < 0)
    {
        perror("fseek");
        return EXIT_FAILURE;
    }
    /*
     * Rien ne garantit que l'on va lire « Une autre ligne »...
     */
    if (fgetws(ligne, sizeof ligne / sizeof ligne[0], fp) == NULL)
    {
        perror("fgetws");
        return EXIT_FAILURE;
    }
    fclose(fp);
    return 0;
}

Notez que dans cet exemple nous passons des chaînes de caractères larges littérales comme arguments à la fonction fputws() . Si cela ne pose le plus souvent pas de problèmes, n’oubliez pas que la table et l’encodage des chaînes de caractères larges littérales sont susceptibles d’être différents de ceux employés par votre système.

La fonction fwide

int fwide(FILE *flux, int mode);

La fonction fwide() permet de connaître l’orientation du flux flux ou d’en attribuer une s’il n’en a pas encore. Dans le cas où mode est un entier positif, la fonction essaye de fixer une orientation « caractères larges », si c’est un nombre négatif, une orientation « multiplets ». Enfin, si mode est nul, alors la fonction détermine simplement l’orientation du flux.

Elle retourne un nombre positif si le flux est orienté caractères larges, un nombre négatif s’il est orienté multiplets et zéro s’il n’a pas encore d’orientation.

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
static void affiche_orientation(FILE *fp, char *nom)
{
    int orientation = fwide(fp, 0);
    fprintf(stderr, "Le flux %s ", nom);
    if (orientation < 0)
        fprintf(stderr, "est orienté multiplets\n");
    else if (orientation > 0)
        fprintf(stderr, "est orienté caractères larges\n");
    else
        fprintf(stderr, "n'a pas encore d'orientation\n");
}
int main(void)
{
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    affiche_orientation(stdout, "stdout");
    printf("Une ligne\n");
    affiche_orientation(stdout, "stdout");
    return 0;
}
Résultat
Le flux stdout n'a pas encore d'orientation
Une ligne
Le flux stdout est orienté multiplets

L’indicateur de conversion ls

Enfin, sachez que les fonctions printf() et scanf() disposent d’un indicateur de conversion permettant d’afficher ou de récupérer des chaînes de caractères larges : l’indicateur ls . Toutefois, il y a une subtilité : la conversion en caractères larges est effectuée avant l’écriture ou après la lecture, dès lors, le flux utilisé doit être orienté multiplets .

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
static void affiche_orientation(FILE *fp, char *nom)
{
    int orientation = fwide(fp, 0);
    fprintf(stderr, "Le flux %s ", nom);
    if (orientation < 0)
        fprintf(stderr, "est orienté multiplets\n");
    else if (orientation > 0)
        fprintf(stderr, "est orienté caractères larges\n");
    else
        fprintf(stderr, "n'a pas encore d'orientation\n");
}
int main(void)
{
    wchar_t ws[255];
    affiche_orientation(stdout, "stdout");
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    if (scanf("%254ls", ws) != 1)
    {
        perror("scanf");
        return EXIT_FAILURE;
    }
    printf("%ls\n", ws);
    affiche_orientation(stdout, "stdout");
    return 0;
}
Résultat
Le flux stdout n'a pas encore d'orientation
Élégant
Élégant
Le flux stdout est orienté multiplets

Comme vous le voyez, nous avons lu et écrit une chaîne de caractères larges, pourtant le flux stdout est orienté multiplets. Cela tient au fait que scanf() lit des caractères et effectue seulement ensuite leur conversions en caractères larges. Inversement printf() converti les caractères larges en caractères avant de les écrire.

Notez que l’inverse est possible à l’aide des fonctions wscanf() et wprintf() .

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
static void affiche_orientation(FILE *fp, char *nom)
{
    int orientation = fwide(fp, 0);
    fprintf(stderr, "Le flux %s ", nom);
    if (orientation < 0)
        fprintf(stderr, "est orienté multiplets\n");
    else if (orientation > 0)
        fprintf(stderr, "est orienté caractères larges\n");
    else
        fprintf(stderr, "n'a pas encore d'orientation\n");
}
int main(void)
{
    char s[255];
    affiche_orientation(stdout, "stdout");
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    if (wscanf(L"%254s", s) != 1)
    {
        perror("wscanf");
        return EXIT_FAILURE;
    }
    wprintf(L"%s\n", s);
    affiche_orientation(stdout, "stdout");
    return 0;
}
Résultat
Le flux stdout n'a pas encore d'orientation
Élégant
Élégant
Le flux stdout est orienté caractères larges

<wchar.h> : les fonctions de manipulation des chaînes de caractères

Le tableau ci-dessous liste quelques fonctions et leur équivalent déclaré dans l’en-tête <wchar.h> .

<string.h> | <wchar.h>
strlen | wcslen
strcpy | wcscpy
strcat | wcscat
strcmp | wcscmp
strchr | wcschr
strpbrk | wcspbrk
strctr | wcsstr
strtok | wcstok

Ces fonctions se comportent de manière identique à leur équivalent, la seule différence est qu’elles manipulent des caractères larges ou des chaînes de caractères larges au lieu de caractères ou de chaînes de caractères.

#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
int
main(void)
{
    char ligne[255];
    if (fgets(ligne, sizeof ligne, stdin) == NULL)
    {
        perror("fgets");
        return EXIT_FAILURE;
    }
    if (setlocale(LC_CTYPE, "") == NULL)
    {
        perror("setlocale");
        return EXIT_FAILURE;
    }
    wchar_t ws[sizeof ligne];
    if (mbstowcs(ws, ligne, sizeof ligne) == (size_t)-1)
    {
        perror("mbstowcs");
        return EXIT_FAILURE;
    }
    printf("strlen : %zu\n", strlen(ligne));
    printf("wcslen : %zu\n", wcslen(ws));
    return 0;
}
Résultat
Élégant
strlen : 10
wcslen : 8

L'en-tête <wctype.h>

Pour terminer, sachez qu’il existe l’en-tête <wctype.h> qui fournit les mêmes fonctions de classification que l’en-tête <ctype.h> , mais pour les caractères larges.

<ctype.h> | <wctype.h>
isupper | iswupper
islower | iswlower
isdigit | iswdigit
isxdigit | iswxdigit
isblank | iswblank
isspace | iswspace
iscntrl | iswcntrl
ispunct | iswpunct
isalpha | iswalpha
isalnum | iswalnum
isgraph | iswgraph
isprint | iswprint
En résumé
  1. Le type entier wchar_t permet de stocker la valeur la plus élevée d’une table de correspondance donnée ;
  2. La conversion des chaînes de caractères littérales en chaînes de caractères littérales larges est assurée par le compilateur ;
  3. Pour les autres chaînes, les fonctions de conversions mbstowcs() et wcstombs() doivent être utilisées ;
  4. La table de correspondance et l’encodage utilisé par le système sont récupérés grâce à la fonction setlocale() ;
  5. Il n’y a pas de solution pour déterminer la table de correspondance et/ou l’encodage utilisés pour écrire les données d’un fichier ;
  6. Etant donné que la table de correspondance et l’encodage utilisés pour représenter les chaînes de caractères littérales et les entrées du terminal sont susceptibles d’être différents, il est parfaitement possible d’obtenir des chaînes de caractères larges différentes ;
  7. Les flux ont une orientation qui détermine s’ils manipulent des caractères ou des caractères larges. Une fois cette orientation fixée, elle ne peut plus être changée ;
  8. Un flux orienté multiplets ne peut pas être utilisé par des fonctions de lecture/écriture larges et, inversement, un flux orienté caractères larges ne peut pas être utilisé par des fonctions de lecture/écriture classiques ;
  9. La fonction fwide() permet de connaître et/ou fixer l’orientation d’un flux ;
  10. L’en-tête <wchar.h> fournit à peu de chose près un équivalent gérant les caractères larges de chaque fonction de lecture/écriture et de manipulation des chaînes de caractères ;
  11. L’en-tête <wctype.h> fournit les mêmes fonctions de classification que l’en-tête <ctype.h> , mais pour les caractères larges.


Les énumérations - Le langage C • Tutoriels • Zeste de Savoir

Les énumérations

Définition

Une énumération se définit à l’aide du mot-clé enum suivi du nom de l’énumération et de ses membres.

enum naturel { ZERO, UN, DEUX, TROIS, QUATRE, CINQ };

La particularité de cette définition est qu’elle crée en vérité deux choses : un type dit « énuméré » enum naturel et des constantes dites « énumérées » ZERO , UN , DEUX , etc. Le type énuméré ainsi produit peut être utilisé de la même manière que n’importe quel autre type. Quant aux constantes énumérées, il s’agit de constantes entières.

Certes me direz-vous, mais que valent ces constantes ? Eh bien, à défaut de préciser leur valeur, chaque constante énumérée se voit attribuer la valeur de celle qui la précède augmentée de un, sachant que la première constante est mise à zéro. Dans notre cas donc, la constante ZERO vaut zéro, la constante UN un et ainsi de suite jusque cinq.

L’exemple suivant illustre ce qui vient d’être dit.

#include <stdio.h>
enum naturel { ZERO, UN, DEUX, TROIS, QUATRE, CINQ };
int main(void)
{
    enum naturel n = ZERO;
    printf("n = %d.\n", (int)n);
    printf("UN = %d.\n", UN);
    return 0;
}
Résultat
n = 0.
UN = 1.

Notez qu’il n’est pas obligatoire de préciser un nom lors de la définition d’une énumération. Dans un tel cas, seules les constantes énumérées sont produites.

enum { ZERO, UN, DEUX, TROIS, QUATRE, CINQ };

Toutefois, il est possible de préciser la valeur de certaines constantes (voire de toutes les constantes) à l’aide d’une affectation.

enum naturel { DIX = 10, ONZE, DOUZE, TREIZE, QUATORZE, QUINZE };

Dans un tel cas, la règle habituelle s’applique : les constantes sans valeur se voit attribuer celle de la constante précédente augmentée de un et celle dont la valeur est spécifiée sont initialisées avec celle-ci. Dans le cas ci-dessus, la constante DIX vaut donc dix, la constante ONZE onze et ainsi de suite jusque quinze. Notez que le code ci-dessous est parfaitement équivalent.

enum naturel { DIX = 10, ONZE = 11, DOUZE = 12, TREIZE = 13, QUATORZE = 14, QUINZE = 15 };

Types entiers sous-jacents

Vous aurez sans doute remarqué que, dans notre exemple, nous avons converti la variable n vers le type int . Cela tient au fait qu’un type énuméré est un type entier (ce qui est logique puisqu’il est censé stocker des constantes entières), mais que le type sous-jacent n’est pas déterminé (cela peut donc être char , short , int ou long ) et dépend entre autre des valeurs devant être contenues. Ainsi, une conversion s’impose afin de pouvoir utiliser un format d’affichage correct.

Pour ce qui est des constantes énumérées, c’est plus simple : elles sont toujours de type int .

Utilisation

Dans la pratique, les énumérations servent essentiellement à fournir des informations supplémentaires via le typage, par exemple pour les retours d’erreurs. En effet, le plus souvent, les fonctions retournent un entier pour préciser si leur exécution s’est bien déroulée. Toutefois, indiquer un retour de type int ne fournit pas énormément d’information. Un type énuméré prend alors tout son sens.

La fonction vider_tampon() du dernier TP s’y prêterait par exemple bien.

enum erreur { E_OK, E_ERR };
static enum erreur vider_tampon(FILE *fp)
{
    int c;
    do
        c = fgetc(fp);
    while (c != '\n' && c != EOF);
    return ferror(fp) ? E_ERR : E_OK;
}

De cette manière, il est plus clair à la lecture que la fonction retourne le statut de son exécution.

Dans la même idée, il est possible d’utiliser un type énuméré pour la fonction statut_jeu() (également employée dans la correction du dernier TP) afin de décrire plus amplement son type de retour.

enum statut { STATUT_OK, STATUT_GAGNE, STATUT_EGALITE };
static enum statut statut_jeu(struct position *pos, char jeton)
{
    if (grille_complete())
        return STATUT_EGALITE;
    else if (calcule_nb_jetons_depuis(pos, jeton) >= 4)
        return STATUT_GAGNE;
    return STATUT_OK;
}

Dans un autre registre, un type enuméré peut être utilisé pour contenir des drapeaux. Par exemple, la fonction traitement() présentée dans le chapitre relatif aux opérateurs de manipulation des bits peut être réecrite comme suit.

enum drapeau {
    PAIR = 0x00,
    PUISSANCE = 0x01,
    PREMIER = 0x02
};
void traitement(int nombre, enum drapeau drapeaux)
{
    if (drapeaux & PAIR) /* Si le nombre est pair */
    {
        /* ... */
    }
    if (drapeaux & PUISSANCE) /* Si le nombre est une puissance de deux */
    {
        /* ... */
    }
    if (drapeaux & PREMIER) /* Si le nombre est premier */
    {
        /* ... */
    }
}
En résumé
  1. Sauf si le nom de l’énumération n’est pas renseignée, une définition d’énumération créer un type énuméré et des constantes énumérées ;
  2. Sauf si une valeur leur est attribuée, la valeur de chaque constante énumérées est celle de la précédente augmentée de un et celle de la première est zéro.
  3. Le type entier sous-jacent à un type énuméré est indéterminé ; les constantes énumérées sont de type int .


Les unions - Le langage C • Tutoriels • Zeste de Savoir

Les unions

Définition

Une union est, à l’image d’une structure, un regroupement d’objet de type différents . La nuance, et elle est de taille, est qu’une union est un agrégat qui ne peut contenir qu' un seul de ses membres à la fois. Autrement dit, une union peut accueillir la valeur de n’importe lequel de ses membres, mais un seul à la fois.

Concernant la définition, elle est identique à celle d’une structure ci ce n’est que le mot-clé union est employé en lieu et place de struct .

union type
{
    int entier;
    double flottant;
    void *pointeur;
    char lettre;
};

Le code ci-dessus défini une union type pouvant contenir un objet de type int ou de type double ou de type pointeur sur void ou de type char . Cette possiblité de ne stocker qu’un objet à la fois est traduite par le résultat de l’opérateur sizeof .

#include <stdio.h>
union type
{
    int entier;
    double flottant;
    void *pointeur;
    char lettre;
};
int main(void)
{
    printf("%u.\n", sizeof (union type));
    return 0;
}
Résultat
8.

Dans notre cas, la taille de l’union correspond à la taille du plus grand type stocké à savoir les types void * et double qui font huit octets. Ceci traduit bien l’impossiblité de stocker plusieurs objets à la fois.

Notez que, comme les structures, les unions peuvent contenir des bits de bourrage, mais uniquement à leur fin.

Pour le surplus, une union s’utilise de la même manière qu’une structure et l’accès aux membres s’effectue à l’aide des opérateurs . et -> .

Utilisation

Étant donné leur singularité, les unions sont rarement employées. Leur principal intérêt est de réduire l’espace mémoire utilisé là où une structure ne le permet pas.

Par exemple, imaginez que vous souhaitiez construire une structure pouvant accueillir plusieurs types possibles, par exemple des entiers et des flottants. Vous aurez besoin de trois champs : un indiquant quel type est stocké dans la structure et deux permettant de stocker soit un entier soit un flottant.

struct nombre
{
    unsigned entier : 1;
    unsigned flottant : 1;
    int e;
    double f;
};

Toutefois, vous gaspiller ici de la mémoire puisque seul un des deux objets sera stockés. Une union est ici la bienvenue afin d’économiser de la mémoire.

struct nombre
{
    unsigned entier : 1;
    unsigned flottant : 1;
    union
    {
        int e;
        double f;
    } u;
};

Le code suivant illustre l’utilisation de cette construction.

#include <stdio.h>
struct nombre
{
    unsigned entier : 1;
    unsigned flottant : 1;
    union
    {
        int e;
        double f;
    } u;
};
static void affiche_nombre(struct nombre n)
{
    if (n.entier)
        printf("%d\n", n.u.e);
    else if (n.flottant)
        printf("%f\n", n.u.f);
}
int main(void)
{
    struct nombre a = { 0 };
    struct nombre b = { 0 };
    a.entier = 1;
    a.u.e = 10;
    b.flottant = 1;
    b.u.f = 10.56;
    affiche_nombre(a);
    affiche_nombre(b);
    return 0;
}
Résultat
10
10.560000

La syntaxe est toutefois un peu pénible puisqu’il est nécessaire d’employer deux fois l’opérateur . : une fois pour accéder aux membres de la structure et une seconde fois pour accéder aux membres de l’union. Par ailleurs, la nécessité d’intégrer l’union comme un champ de la structure, et donc de lui donner un nom, est également ennuyeux.

Heureusement pour nous, il est possible de rendre l’union « anonyme », c’est-à-dire de l’inclure comme champ de la structure, mais sans lui donner un nom. Dans un tel cas, les champs de l’union sont traîtés comme s’ils étaient des champs de la structure et sont donc accessibles comme tel.

#include <stdio.h>
struct nombre
{
    unsigned entier : 1;
    unsigned flottant : 1;
    union
    {
        int e;
        double f;
    };
};
static void affiche_nombre(struct nombre n)
{
    if (n.entier)
        printf("%d\n", n.e);
    else if (n.flottant)
        printf("%f\n", n.f);
}
int main(void)
{
    struct nombre a = { 0 };
    struct nombre b = { 0 };
    a.entier = 1;
    a.e = 10;
    b.flottant = 1;
    b.f = 10.56;
    affiche_nombre(a);
    affiche_nombre(b);
    return 0;
}

Notez que cette possibilité ne se limite pas aux unions, il est possible de faire de même avec des structures.

En résumé
  1. Une union est un agrégat regroupant des objets de types différents, mais ne pouvant en stocker qu’un seul à la fois ;
  2. Comme les structures, les unions peuvent comprendre des bits de bourrage, mais uniquement à leur fin ;
  3. Il est possible de simplifier l’accès aux champs d’une union lorsque celle-ci est elle-même un champ d’une autre structure ou d’une autre union en la rendant « anonyme ».


Les définitions de type - Le langage C • Tutoriels • Zeste de Savoir

Les définitions de type

Définition et utilisation

Une définition de type permet, comme son nom l’indique, de définir un type, c’est-à-dire d’en produire un nouveau ou, plus précisément, de créer un alias (un synonyme si vous préférez) d’un type existant. Une définition de type est identique à une déclaration de variable, si ce n’est que celle-ci doit être précédée du mot-clé typedef (pour type definition ) et que l’identificateur ainsi choisi désignera un type et non une variable.

Ainsi, le code ci-dessous défini un nouveau type entier qui sera un alias pour le type int .

typedef int entier;

Le synonyme ainsi créé peut être utilisé au même endroit que n’importe quel autre type.

#include <stdio.h>
typedef int entier;
entier main(void)
{
    entier a = 10;
    printf("%d.\n", a);
    return 0;
}

D’accord, mais cela me sert à quoi de créer un synonyme pour un type existant ? :euh:

Les définitions de type permettent en premier lieu de raccourcir certaines écritures, notamment afin de s’affranchir des mots-clés struct , union et enum (bien que, ceci constitue une pertes d’information aux yeux de certains).

#include <stdio.h>
struct position
{
    int lgn;
    int col;
};
typedef struct position position;
int main(void)
{
    position pos = { .lgn = 1, .col = 1 };
    printf("%d, %d.\n", pos.lgn, pos.col);
    return 0;
}

Notez qu’une définition de type étant une délaration, il est parfaitement possible de combiner la définition de la structure et la définition de type (comme pour une variable, finalement).

typedef struct position
{
    int lgn;
    int col;
} position;

Également, dans le même sens que ce qui a été dit au sujet des énumérations, une définition de type peut être employée pour donner plus d’informations via le typage. C’est une manière de désigner plus finement le contenu d’un type en ne se contentant pas d’une information plus générale comme « un entier » ou « un flottant ».

Par exemple, nous aurions pu définir un type ligne et un type colonne afin de donner plus d’information sur le contenu de nos variables.

typedef short ligne;
typedef short colonne;
struct position
{
    ligne lgn;
    colonne col;
};

De même, les définitions de type sont couramment utilisée afin de créer des asbstractions. Nous en avons vu un exemple avec l’en-tête <time.h> qui défini le type time_t . Celui-ci permet de ne pas devoir modifier la fonction time() et son utilisation suivant le type qui est sous-jacent. Peu importe que time_t soit un entier ou un flottant, la fonction time() s’utilise de la même manière.

Enfin, les définitions de type permettent de résoudre quelques points de syntaxe tordus.

En résumé
  1. Une définition de type permet de créer un synonyme d’un type existant (en ce compris un autre alias ) ;


Les pointeurs de fonction - Le langage C • Tutoriels • Zeste de Savoir

Les pointeurs de fonction

Déclaration et initialisation

Jusqu’à maintenant, nous avons manipulé des pointeurs sur objet, c’est-à-dire des adresses vers des zones mémoires contenant des données (des entiers, des flottants, des structures, etc.). Toutefois, il est également possible de référencer des instructions et ceci est réalisé en C à l’aide des pointeurs de fonction.

Un pointeur de fonction se définit à l’aide d’une syntaxe mélangeant celle des pointeurs sur tableau et celles des prototypes de fonction. Sans plus attendre, voici ci-dessous la définition d’un pointeur sur une fonction retournant un int et attendant un int comme argument.

int (*pf)(int);

Comme vous le voyez, il est nécessaire, tout comme les pointeurs sur tableau, d’entourer le symbole * et l’identificateur de parenthèses, ici afin d’éviter que cette déclaration ne soit vue comme un prototype et non comme un pointeur de fonction. Autre particularité : le type de retour, le nombre d’arguments et leur type doivent également être spécifiés.

Initialisation

Ok… Et je lui affecte comment l’adresse d’une fonction, moi, à ce machin ? D’ailleurs, elles ont une adresse, les fonctions ? :euh:

Oui et comme d’habitude, cela est réalisé à l’aide de l’opérateur & .
En fait, dans le cas des fonctions, il n’est pas obligatoire de recourir à cet opérateur, ainsi, les deux syntaxes suivantes sont correctes.

int (*pf)(int);
pf = &fonction;
pf = fonction;

Ceci est dû, à l’image des tableaux, à une conversion implicite : sauf s’il est l’opérande de l’opérateur & , un identificateur de fonction est converti en un pointeur sur cette fonction. L’utilisation de l’opérateur & est donc facultative, mais elle a le mérite de clarifier un peu les choses. Pour cette raison, nous utiliserons cette syntaxe dans la suite de ce cours.

Utilisation

Déréférencement

Un pointeur de fonction s’emploie de la même manière qu’un pointeur classique, si ce n’est que l’opérateur * et l’identificateur doivent à nouveau être entre parenthèses. Pour le reste, la liste des arguments suit l’expression déréférencée, comme pour un appel de fonction classique.

#include <stdio.h>
static int triple(int a)
{
    return a * 3;
}
int main(void)
{
    int (*pt)(int) = &triple;
    printf("%d.\n", (*pt)(3));
    return 0;
}
Résultat
9.

Toutefois, particularité des fonctions oblige, sachez que le déréférencement n' est pas nécessaire . Ceci à cause de la conversion implicite expliquée précédemment : un identificateur de fonction est, sauf s’il est l’opérande de l’opérateur & , converti en un pointeur sur cette fonction. L’appel triple(3) cache donc en fait un pointeur de fonction qui, comme vous le voyez, n’est pas déréférencé.

Heu … Mais pourquoi l’expression (*pt)(3) ne provoque-t-elle pas une erreur si c’est un pointeur de fonction qui est nécessaire lors d’un appel ?

Parce que la conversion implicite aura lieu juste après le déréférencement. :-°
Eh oui, déréférencé un pointeur de fonction, c’est un peu reculer pour mieux sauter : nous obtenons une expression équivalente à un identificateur de fonction qui sera ensuite convertie en un pointeur de fonction. Les deux expressions suivantes sont donc équivalentes.

triple(3);
(*pt)(3);

L’intérêt d’employer le déréférencement est purement syntaxique : cela permet de distinguer des appels effectuer via des pointeurs des appels de fonction classiques.

Passage en argument

Comme n’importe quel pointeur, un pointeur de fonction peut être passé en argument d’une autre fonction (c’est d’ailleurs tout l’intérêt de ceux-ci, comme nous le verrons bientôt). Pour ce faire, il vous suffit d’employer la même syntaxe que pour une déclaration.

#include <stdio.h>
static int triple(int a)
{
    return a * 3;
}
static int quadruple(int a)
{
    return a * 4;
}
static void affiche(int a, int (*pf)(int))
{
    printf("%d.\n", (*pf)(a));
}
int main(void)
{
    affiche(3, &triple);
    affiche(3, &quadruple);
    return 0;
}
Résultat
9.
12.

Dans l’exemple ci-dessus, les fonctions triple() et quadruple() sont utilisées comme des fonctions de rappel ( callback function en anglais), c’est-à-dire que leur adresse est passée à la fonction affichage() de sorte que cette dernière puisse les appeler par la suite.

Retour de fonction

Dans l’autre sens, il est possible de retourner un pointeur de fonction à l’aide d’une syntaxe… un peu lourde. :-°

#include <stddef.h>
#include <stdio.h>
static void affiche_pair(int a)
{
    printf("%d est pair.\n", a);
}
static void affiche_impair(int a)
{
    printf("%d est impair.\n", a);
}
static void (*affiche(int a))(int)
{
    if (a % 2 == 0)
        return &affiche_pair;
    else
        return &affiche_impair;
}
int main(void)
{
    void (*pf)(int);
    int a = 2;
    pf = affiche(a);
    (*pf)(a);
    return 0;
}
Résultat
2 est pair.

Comme pour une variable de type pointeur de fonction, le symbole * doit être entouré de parenthèses ainsi que l’identificateur qui le suit. Toutefois, lorsqu’il s’agit du type de retour d’une fonction, la liste des arguments doit également être placée entre ces parenthèses.

Dans cet exemple, la fonction affiche() attend un int et retourne un pointeur sur une fonction ne retournant rien et utilisant un argument de type int . Suivant si a est pair ou impair, la fonction affiche() retourne l’adresse de la fonction affichage_pair() ou affichage_impair() qui est recueillie par le pointeur pf de la fonction main() .

Pointeurs de fonction et pointeurs génériques

Vous le savez, le type void est employé en C pour produire un pointeur générique qui peut se voir assigner n’importe quel type de pointeur et être converti vers n’importe quel type de pointeurs. Cette définition est toutefois incomplète car il doit en fait être précisé que cela ne fonctionne que pour des pointeurs sur des objets . Le code ci-dessous est donc incorrect.

int (*pf)(int);
void *p;
pf = p; /* Faux. */
p = pf; /* Faux également. */

Pareillement, une conversion explicite d’un pointeur sur un objet vers ou depuis un pointeur sur fonction est interdite (ou, plus précisément, son résultat est indéterminé). Ceci exclut donc l’utilisation de l’indicateur p de la fonction printf() pour afficher un pointeur de fonction.

printf("%p.\n", (void *)pf); /* Faux. */

Toutefois, les pointeurs de fonction disposent de leur propre pointeur « générique ». Nous utilisons ici les guillemets car il ne l’est pas tout à fait puisqu’il peut notamment être utilisé à l’inverse d’un pointeur sur void . Un pointeur « générique » de fonction se déclare comme un pointeur de fonction, mais en ne spécifiant que le type de retour.

int (*pf)();

Un tel pointeur peut se voir assigner n’importe quel pointeur sur fonction du moment que le type de retour de celui-ci est identique au sien. Inversement, ce pointeur « générique » peut être affecté à un autre pointeur sous la même condition. Dans notre cas, le type de retour doit donc être int .

int (*f)(int, int);
int (*g)(char, char, double);
void (*h)(void);
int (*pf)();
pf = f; /* Ok. */
pf = g; /* Ok. */
pf = h; /* Faux car le type de retour de `h` est `void`. */
f = pf; /* Ok. */

Il existe cependant une exception supplémentaire : une fonction à nombre variables d’arguments ne peut pas être affectée à un tel pointeur, même si le type de retour est identique. Nous verrons bientôt de quoi il s’agit, mais pour l’heure, sachez que les fonctions de la famille de printf() et de scanf() sont concernées par cette règle.

La promotion des arguments

Hé là, minute papillon ! Il se passe quoi si j’utilise le pointeur pf avec une fonction qui attend normalement des arguments ?

Excellente question ! :)

Vous vous en doutez : les arguments peuvent toujours être envoyé à la fonction référencée. Cependant, il y a une subtilité. Étant donné qu’un pointeur « générique » de fonction ne fournit aucune information quant aux arguments, le compilateur ne peut pas convertir ceux-ci vers le type attendu par la fonction. Ainsi, si vous fournissez un int et que la fonction attend un char , le int ne sera pas converti vers le type char par le compilateur.

Toutefois, dans un tel cas, plusieurs conversions implicites sont appliquées afin de limiter les types possibles (on parle de « promotion des arguments ») :

  1. Un argument de type entier de rang inférieur ou égal à celui du type int (soit char et short le plus souvent) est converti vers le type int (ou unsigned int si le type int ne peut pas représenter toutes les valeurs du type d’origine).
  2. Un argument de type float et converti vers le type double .

Ceci signifie qu’une fonction appelée à l’aide d’un pointeur « générique » de fonction ne pourra jamais recevoir des arguments de type char , short ou float .

Illustration.

#include <stdio.h>
static float triple(float f)
{
    return 3.F * f;
}
static short quadruple(short n)
{
    return 4 * n;
}
int main(void)
{
    float (*pt)() = &triple;
    short (*pq)() = &quadruple;
    printf("triple = %f.\n", (*pt)(3.F)); /* Faux. */
    printf("quadruple = %d.\n", (*pq)(2)); /* Faux. */
    return 0;
}

Les pointeurs nuls

L’absence de conversions par le compilateur dans le cas où aucune information n’est fournie par rapport aux arguments pose un problème particulier dans le cas des pointeurs nuls et tout spécialement lors de l’usage de la macroconstante NULL .

Rappelez-vous : un pointeur nul est construit en convertissant, soit explicitement, soit implicitement, zéro (entier) vers un type pointeur. Or, étant donné que le compilateur n’effectuera aucune conversion implicite dans notre cas, nous ne pouvons compter que sur les conversions explicites.

Et c’est ici que le bât blesse : la macroconstante NULL a deux valeurs possibles : (void *)0 ou 0 , le choix étant laissé aux différents systèmes. La première ne pose pas de problème, mais la seconde en pose un plutôt gênant : c’est un int qui sera passé comme argument et non un pointeur nul.

Dès lors, lorsque vous employez un pointeur « générique » de fonction, vous devez recourir à une conversion explicite si vous souhaitez produire un pointeur nul.

static void affiche(char *chaine)
{
    if (chaine != NULL)
        puts(chaine);
}
/* ... */
void (*pf)() = &affiche;
(*pf)(NULL); /* Faux. */
(*pf)(0); /* Faux. */
(*pf)((char*)0); /* Ok. */
En résumé
  1. Un pointeur de fonction permet de stocker une référence vers une fonction ;
  2. Il n’est pas nécessaire d’employer l’opérateur & pour obtenir l’adresse d’une fonction ;
  3. Le déréférencement n’est pas obligatoire lors de l’utilisation d’un pointeur de fonction ;
  4. Une fonction employant un pointeur vers une autre fonction reçu en argument est appelée une fonction de rappel ( callback function en anglais) ;
  5. Il est possible d’utiliser un pointeur « générique » de fonction en ne fournissant aucune information quant aux arguments lors de sa définition ;
  6. Un pointeur « générique » de fonction peut être converti vers ou depuis n’importe quel autre type de pointeur de fonction du moment que le type de retour reste identique ;
  7. Lors de l’utilisation d’un pointeur « générique » de fonction, les arguments transmis sont promus, mais aucune conversion implicite n’est réalisée par le compilateur ;
  8. Lors de l’utilisation d’un pointeur « générique » de fonction, un pointeur nul ne peut être fourni qu’à l’aide d’une conversion explicite.


Les fonctions et macrofonctions à nombre variable d'arguments - Le langage C • Tutoriels • Zeste de Savoir

Les fonctions et macrofonctions à nombre variable d'arguments

Présentation

Une fonction à nombre variable d’arguments est, comme son nom l’indique, une fonction capable de recevoir et de manipuler un nombre variable d’arguments, mais en plus, et cela son nom ne le spécifie pas, potentiellement de types différents.

Une telle fonction se définit en employant comme dernier paramètre une suite de trois points appelée une ellipse indiquant que des arguments supplémentaires peuvent être transmis.

void affiche_suite(int n, ...);

Le prototype ci-dessus déclare une fonction recevant un int et, éventuellement, un nombre indéterminé d’autres arguments. La partie précédant l’ellipse décrit donc les paramètres attendus par la fonction et qui, comme pour n’importe quelle autre fonction, doivent lui être transmis.

Une fonction à nombre variable d’arguments doit toujours avoir au moins un paramètre non optionnel.

L’ellipse ne peut être placée qu’à la fin de la liste des paramètres.

Une fois définie, la fonction peut être appelée en lui fournissant zéro ou plusieurs arguments supplémentaires.

affiche_suite(10, 20);
affiche_suite(10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
affiche_suite(10);

Comme pour les pointeurs génériques de fonction, le nombre et le types des arguments est inconnu du compilateur (c’est d’ailleurs bien le but de la manœuvre). Dès lors, les même règles de promotion s’appliquent ainsi que les problèmes qui y sont inhérents, notamment la problématique des pointeurs nuls. N’employer donc pas la macroconstante NULL pour fournir un pointeur nul comme argument optionnel.

L'en-tête <stdarg.h>

Une fois le prototype de la fonction déterminé, encore faut-il pouvoir manipuler ces arguments supplémentaires au sein de sa définition. Pour ce faire, la bibliothèque standard nous fourni trois macrofonctions : va_start() , va_arg() et va_end() définie dans l’en-tête <stdarg.h> . Ces trois macrofonctions attendent comme premier argument une variable de type va_list définit dans le même en-tête.

Afin d’illustrer leur fonctionnement, nous vous proposons directement un exemple mettant en œuvre une fonction affiche_suite() qui reçoit comme premier paramètre le nombre d’entiers qui vont lui être transmis et les affiche ensuite tous, un par ligne.

#include <stdarg.h>
#include <stdio.h>
void affiche_suite(int nb, ...)
{
    va_list ap;
    va_start(ap, nb);
    while (nb > 0)
    {
        int n;
        n = va_arg(ap, int);
        printf("%d.\n", n);
        --nb;
    }
    va_end(ap);
}
int main(void)
{
    affiche_suite(10, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100);
    return 0;
}
Résultat
10.
20.
30.
40.
50.
60.
70.
80.
90.
100.

La macrofonction va_start initialise le parcours des paramètres optionnels. Elle attend deux arguments : une variable de type va_list et le nom du dernier paramètre obligatoire de la fonction courante (dans notre cas nb ). Il est impératif de l’appeler avant toute opération sur les paramètres optionnels.

La marcofonction va_arg() retourne le paramètre optionnel suivant en considérant celui-ci comme de type type . Elle attend deux arguments : une variable de type va_list précédemment initialisée par la macrofonction va_start() et le type du paramètre optionnel suivant.

Enfin, la fonction va_end() met fin au parcours des arguments optionnels. Elle doit toujours être appelée après un appel à la macrofonction va_start() .

La macrofonction va_arg() n’effectue aucune vérification ! Cela signifie que si vous renseigner un type qui ne correspond pas au paramètre optionnel suivant ou si vous tentez de récupérer un paramètre optionnel qui n’existe pas, vous allez au-devant de comportements indéfinis.

Étant donné cet état de fait, il est impératif de pouvoir déterminer le nombre d’arguments optionnels envoyé à la fonction. Dans notre cas, nous avons opté pour l’emploi d’un paramètre obligatoire indiquant le nombre d’arguments optionnels envoyés.

Mais ?! Et si l’utilisateur de la fonction se plante en ne précisant pas le bon nombre ou le bon type d’arguments ?

Eh bien… C’est foutu. :-°
Il s’agit là d’une lacune similaire à celle des pointeurs « génériques » de fonction : étant donné que le type et le nombre des arguments optionnels est inconnu, le compilateur ne peut effectuer aucune vérification ou conversion.

Méthodes pour déterminer le nombre et le type des arguments

De manière générale, il existe trois grandes méthodes pour gérer le nombre et le type des arguments optionnels.

Les chaînes de formats

Cette méthode, vous la connaissez déjà, il s’agit de celle employée par les fonctions printf() , scanf() et consœurs. Elle consiste à décrire le nombre et le type des arguments à l’aide d’une chaîne de caractères comprenant des indicateurs.

Les suites d’arguments de même type

La seconde solution ne peut être utilisée que si tous les arguments optionnels sont de même type. Elle consiste soit à indiquer le nombre d’arguments transmis, soit à utiliser un délimitateur. La fonction affiche_suite() recourt par exemple à un paramètre pour déterminer le nombre de paramètres optionnels qu’elle a reçu.

Un délimitateur pourrait par exemple être un pointeur nul dans le cas d’une fonction similaire affichant une suite de chaîne de caractères.

#include <stdarg.h>
#include <stddef.h>
#include <stdio.h>
static void affiche_suite(char *chaine, ...)
{
    va_list ap;
    va_start(ap, chaine);
    do
    {
        puts(chaine);
        chaine = va_arg(ap, char *);
    } while(chaine != NULL);
    va_end(ap);
}
int main(void)
{
    affiche_suite("un", "deux", "trois", (char *)0);
    return 0;
}
Résultat
un
deux
trois

Emploi d’un pivot

La dernière pratique consiste à recourir à un paramètre « pivot » qui fera varier le parcours des paramètres optionnels en fonction de sa valeur. Notez qu’un type énuméré se prête très bien pour ce genre de paramètre. Par exemple, la fonction suivante affiche un int ou un double suivant la valeur du paramètre type .

#include <stdarg.h>
#include <stdio.h>
enum type { TYPE_INT, TYPE_DOUBLE };
static void affiche(enum type type, ...)
{
    va_list ap;
    va_start(ap, type);
    switch (type)
    {
    case TYPE_INT:
        printf("Un entier : %d.\n", va_arg(ap, int));
        break;
    case TYPE_DOUBLE:
        printf("Un flottant : %f.\n", va_arg(ap, double));
        break;
    }
    va_end(ap);
}
int main(void)
{
    affiche(TYPE_INT, 10);
    affiche(TYPE_DOUBLE, 3.14);
    return 0;
}
Résultat
Un entier : 10.
Un flottant : 3.140000.

Les macrofonctions à nombre variable d'arguments

Vous vous en doutez en lisant le titre de cette section, il est également possible, à l’image des fonctions, de construire des macrofonctions à nombre variable d’arguments. Comme pour les fonctions, une macrofonction à nombre variable d’arguments se définit en indiquant une suite de trois points comme dernier argument.

Toutefois, à l’inverse des fonctions, la suite d’arguments variables ne peut être utilisée que d’un seul tenant à l’aide de la macroconstante __VA_ARGS__ .

Ainsi, avec la définition ci-dessous, l’appel PRINTF("%d, %d, %d\n", 1, 2, 3) sera remplacé par printf(("%d, %d, %d\n"), 1, 2, 3) .

#define PRINTF(fmt, ...) printf((fmt), __VA_ARGS__)

Notez qu’à l’inverse d’une fonction, une macrofonction ne requiert pas d’argument non optionnel. La définition suivante est donc parfaitement valide.

#define PRINTF(...) printf(__VA_ARGS__) /* Ok. */
En résumé
  1. Une fonction à nombre variable d’arguments est capable de recevoir et de manipuler une suite indéterminée d’arguments optionnels de types potentiellement différents.
  2. L’ellipse ne peut être placée qu’à la fin de la liste des paramètres et doit impérativement être précédée d’un paramètre non optionnel.
  3. La macrofonction va_arg() n’effectue aucune vérification, il est donc impératif de contrôler le nombre et le types des arguments reçus, par exemple à l’aide d’une chaîne de formats ou d’un paramètre décrivant le nombre d’arguments transmis.
  4. Il est possible de construire une macrofonction à nombre variable d’arguments en ajoutant une ellipse à la fin de sa liste d’argument.
  5. Les arguments optionnels sont utilisables dans la définition de la macrofonction en employant la macroconstante __VA_ARGS__ .
  6. Une macrofonction ne requiert pas d’argument non optionnel.


Les sélections génériques - Le langage C • Tutoriels • Zeste de Savoir

Les sélections génériques

Définition et utilisation

Les sélections génériques sont assez singulières en ce qu’elles permettent de construire des expressions en fonction du type d’une autre expression. Syntaxiquement, elles se rapprochent d’un mélange entre l’opérateur conditionnel et le switch .

_Generic(expression, type1: expression1, type2: expression2, default: expression3)

Dans cet exemple, le type de l’expression expression est évalué et, suivant celui-ci, la sélection générique est « remplacée » (le terme est impropre, mais il illustre bien le résultat obtenu) par expression1 , expression2 ou expression3 . Comme pour les switch , il est possible d’employer le mot-clé default en lieu et place d’un type afin de signaler que si aucun type ne correspond à celui de l’expression expression , alors c’est l’expression désignée par le mot-clé default qui doit être choisie.

Un exemple valant mieux qu’un long discours, voici un exemple de sélection générique affichant le type de son expression de contrôle.

#include <stdio.h>
int
main(void)
{
    int x;
    printf("type de x : %s\n", _Generic(x, char: "char", short: "short", int: "int", double: "double", default: "?"));
    return 0;
}
Résultat
type de x : int

Comme vous le voyez, la sélection générique sera « remplacée » par une des chaînes de caractères de la sélection suivant le type de la variable x , dans ce cas ci "int" . Ce genre de sélections est toutefois plus utile au sein d’une macrofonction afin de pouvoir être réutilisée.

#include <stdio.h>
#define TYPEOF(x) _Generic((x), \
    char: "char", \
    short: "short", \
    int: "int", \
    long: "long", \
    long long: "long long", \
    float: "float", \
    double: "double", \
    long double: "long double")
int
main(void)
{
    int x;
    printf("type de x : %s\n", TYPEOF(x));
    return 0;
}

Néanmoins, une des grandes forces et particularités des sélections génériques tient au fait qu’elles peuvent être employées comme n’importe quel élément d’une expression (du moment que le résultat final est valide, bien entendu). Ainsi, une sélection générique peut finalement produire une constante (comme nous l’avons démontré ci-dessus), mais également un identificateur de variable ou de fonction.

#include <errno.h>
#include <limits.h>
#include <stdio.h>
#define add(x, y) _Generic((x) + (y), int: safe_add, long: safe_addl)((x), (y))
int safe_add(int a, int b)
{
    int err = 1;
    if (b >= 0 && a > INT_MAX - b)
        goto overflow;
    else if (b < 0 && a < INT_MIN - b)
        goto underflow;
    return a + b;
underflow:
    err = -1;
overflow:
    errno = ERANGE;
    return err > 0 ? INT_MAX : INT_MIN;
}
long safe_addl(long a, long b)
{
    int err = 1;
    if (b >= 0 && a > LONG_MAX - b)
        goto overflow;
    else if (b < 0 && a < LONG_MIN - b)
        goto underflow;
    return a + b;
underflow:
    err = -1;
overflow:
    errno = ERANGE;
    return err > 0 ? LONG_MAX : LONG_MIN;
}
int
main(void)
{
    printf("%d\n", add(INT_MAX, 45));
    printf("%ld\n", add(LONG_MAX, 1));
    return 0;
}
Résultat
2147483647
9223372036854775807

Dans l’exemple ci-dessus, nous avons repris la fonction safe_add() du chapitre sur les limites des types et avons ajouté la fonction safe_addl() qui est la même, mais pour le type long . Nous utilisons ensuite une sélection générique pour appeler la bonne fonction suivant le type de l’expression x + y . Vous remarquerez que dans ce cas ci, la sélection générique est « remplacée » par un identificateur de fonction ( safe_add ou safe_addl ) et que ce dernier peut être utilisé comme n’importe quel autre identificateur de fonction, par exemple pour appeler une fonction en lui précisant des arguments.

Évaluation

L’expression de contrôle d’une sélection générique n’est pas évaluée. Autrement dit, dans l’exemple précédent, l’addition n’est pas effectuée, seul son type est déterminé.

Si aucune entrée de la sélection générique ne correspond au type de l’expression de contrôle et qu’il n’y a pas d’entrée default , alors la compilation échouera.

En résumé
  1. Une sélection générique permet de construire une expression en fonction du type d’une autre expression.
  2. Il est possible d’utiliser le mot-clé default pour définir une expression à utiliser en cas de non correspondance des autres types.
  3. Une sélection générique peut être utilisée comme n’importe quel élément d’une expression.
  4. Dans le cas où il n’y a pas de correspondance entre le type de l’expression de contrôle et les entrées de la sélection et que le mot-clé default n’a pas été utilisé, la compilation échoue.


T.P. : un allocateur statique de mémoire - Le langage C • Tutoriels • Zeste de Savoir

T.P. : un allocateur statique de mémoire

Objectif

Votre objectif sera de parvenir à réaliser un petit allocateur statique de mémoire en mettant en œuvre deux fonctions : static_malloc() et static_free() comparables aux fonctions malloc() et free() de la bibliothèque standard. Toutefois, afin d’éviter d’entrer dans les méandres de l’allocation dynamique de mémoire, ces fonctions fourniront de la mémoire préalablement allouée statiquement.

Première étape : allouer de la mémoire

Pour commencer, vous allez devoir construire une fonction static_malloc() dont le prototype sera le suivant.

void *static_malloc(size_t taille);

À la lumière de la fonction malloc() , celle-ci reçoit une taille en multiplet et retourne un pointeur vers un bloc de mémoire d’au moins la taille demandée ou un pointeur nul s’il n’y a plus de mémoire disponible.

Mémoire statique

Afin de réaliser ses allocations, la fonction static_malloc() ira piocher dans un bloc de mémoire statique. Celui-ci consistera simplement en un tableau de char de classe de stockage statique d’une taille prédéterminée. Pour cet exercice, nous partirons avec un bloc de un mébimultiplets, soit 1.048.576 multiplets (1024 × 1024).

static char mem[1048576UL];

Alignement

Toutefois, retourner un bloc de char de la taille demandée ne suffit pas. En effet, si nous allouons par exemple treize multiplets, disons pour une chaîne de caractères, puis quatre multiplets pour un int , nous allons retourner une adresse qui ne respecte potentiellement pas l’alignement requis par le type int . Étant donné que la fonction static_malloc() ne connaît pas les contraintes d’alignements que doit respecter l’objet qui sera stocké dans le bloc qu’elle fournit, elle doit retourner un bloc respectant les contraintes les plus strictes. Dit autrement, la taille de chaque bloc devra être un multiple de l’alignement le plus rigoureux.

Cet alignement peut être connu à l’aide de l’opérateur _Alignof et du type spécial max_align_t (définit dans l’en-tête <stddef.h> ).

#include <stddef.h>
#include <stdio.h>
int
main(void)
{
    printf("L'alignement le plus strict est : %zu\n", _Alignof(max_align_t));
    return 0;
}
Résultat
L'alignement le plus strict est : 16

Mais… ce n’est pas tout ! Le tableau alloué statiquement doit lui aussi être aligné suivant l’alignement le plus sévère. En effet, si celui-ci commence à une adresse non multiple de cet alignement, notre stratégie précédente tombe à l’eau. Pour ce faire, nous pouvons utiliser le spécificateur _Alignas qui permet de modifier les contraintes d’alignement d’un type 1.

static _Alignas(max_align_t) char mem[1048576UL];

Ici nous indiquons donc que nous souhaitons que le tableau mem ait le même alignement que le type max_align_t , soit l’alignement le plus sévère.

Avec ceci, vous devriez pouvoir réaliser la fonction static_malloc() sans encombre.
Hop hop ! Au travail ! :)

  1. Le spécificateur _Alignas a été introduit par la norme C11. Auparavant, pour modifier l’alignement d’un type, il était nécessaire d’employer une union comprenant un champ de ce type et un champ du type dont on souhaite copier l’alignement. Pour obtenir l’alignement le plus strict, la même technique était employée, mais en utilisant les types avec les alignements les plus sévères (comme long double , long long et void * ).

    ↩

Correction

Afficher/Masquer le contenu masqué
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#define MEM_MAX (1024UL * 1024UL)
#define ALIGN_MAX (_Alignof(max_align_t))
static _Alignas(max_align_t) char mem[MEM_MAX];
static size_t calcule_multiple_align_max(size_t);
static void *static_malloc(size_t);
static size_t calcule_multiple_align_max(size_t a)
{
    /*
     * Calcule le plus petit multiple de l'alignement maximal égal ou supérieur à `a`.
     */
    return (a % ALIGN_MAX == 0) ? a : a + ALIGN_MAX - (a % ALIGN_MAX);
}
static void *static_malloc(size_t taille)
{
    /*
     * Alloue un bloc de mémoire au moins de taille `taille`.
     */
    static size_t alloue = 0UL;
    void *ret;
    assert(taille > 0);
    taille = calcule_multiple_align_max(taille);
    if (MEM_MAX - alloue < taille)
    {
        fprintf(stderr, "Il n'y a plus assez de mémoire disponible.\n");
        return NULL;
    }
    ret = &mem[alloue];
    alloue += taille;
    return ret;
}

La fonction calcule_multiple_align_max() détermine le plus petit multiple de l’alignement maximal égal ou supérieur à a . Celle-ci nous permet d’« arrondir » la taille demandée de sorte que les blocs alloués soit toujours correctement alignés.

La fonction static_malloc() utilise une variable statique : alloue qui représente la quantité de mémoire déjà allouée. Dans le cas où la taille demandée (arrondie au multiple de l’alignement qui lui est égal ou supérieur) n’est pas disponible, un pointeur nul est retourné. Sinon, la variable alloue est mise à jour et un pointeur vers la position courante du tableau mem est retourné.

Notez que nous avons utilisé une assertion afin d’éviter qu’une taille nulle ne soit fournie.

Deuxième étape : libérer de la mémoire

Pouvoir allouer de la mémoire, c’est bien, mais pouvoir en libérer, ce serait mieux. Parce que pour le moment, dès que l’on arrive à la fin du tableau, que la mémoire précédemment allouée soit encore utilisée ou non, c’est grillé. :-°

Toutefois, pour mettre en place un système de libération de la mémoire, il va nous falloir changer un peu de tactique.

Ajout d’un en-tête

Tout d’abord, si nous voulons libérer des blocs puis les réutiliser, il nous faut conserver d’une manière ou d’une autre leur taille. En effet, sans cette information, il nous sera impossible de les réaffecter.

Pour ce faire, il nous est possible d’ajouter un en-tête lors de l’allocation d’un bloc, autrement dit un objet (par exemple un entier de type size_t ) qui précédera le bloc alloué et contiendra la taille du bloc.

+---------+-----------------+
| En-tête |   Bloc alloué   |
+---------+-----------------+

Ainsi, lors de l’allocation, il nous suffit de transmettre à l’utilisateur le bloc alloué sans son en-tête à l’aide d’une expression du type (char *)ptr + sizeof (size_t) et, à l’inverse, lorsque l’utilisateur souhaite libérer un bloc, de le récupérer à l’aide d’une expression comme (char *)ptr - sizeof (size_t) . Notez que cette technique nous coûte un peu plus de mémoire puisqu’il est nécessaire d’allouer le bloc et l’en-tête.

Les listes chaînées

Mais, ce n’est pas tout. Pour pouvoir retrouver un bloc libéré, il nous faut également un moyen pour les conserver et en parcourir la liste. Afin d’atteindre cet objectif, nous allons employer une structure de donnée appelée une liste chaînée. Celle-ci consiste simplement en une structure comprenant des données ainsi qu’un pointeur vers une structure du même type.

struct list
{
    struct list *suivante;
};

Ainsi, il est possible de créer des chaines de structure, chaque maillon de la chaîne référencant le suivant. L’idée pour notre allocateur va être de conserver une liste des blocs libérés, liste qu’il parcourera en vue de rechercher un bloc de taille suffisante avant d’allouer de la mémoire provenant de la réserve. De cette manière, il nous sera possible de réutiliser de la mémoire précédemment allouée avant d’aller puiser dans la réserve.

Nous aurons donc a priori besoin d’une liste comprenant une référence vers le bloc libéré et une référence vers le bloc suivant (il ne nous est pas nécessaire d’employer un membre pour la taille du bloc puisque l’en-tête la contient).

struct bloc
{
    void *p;
    struct bloc *suivant;
};

Le serpent qui se mange la queue

Cependant, avec une telle technique, nous risquons d’entrer dans un cercle vicieux. En effet, imaginons qu’un utilisateur libère un bloc de 64 multiplets. Pour l’ajouter à la liste des blocs libérés, nous avons besoin d’espace pour stocker une structure bloc , nous allons donc allouer un peu de mémoire. De plus, si par la suite ce bloc est réutilisé, la structure bloc ne nous est plus utile, sauf que si nous la libérons, nous allons devoir allouer une autre structure bloc pour référencé… une structure bloc (à moins que la structure ne s’autoréférence). Voilà qui n’est pas très optimal.

À la place, il nous est possible de recourir à une autre stratégie : inclure la structure bloc dans l’en-tête ou, plus précisémment, son champ suivant , la référence vers le bloc n’étant plus nécessaire dans ce cas. Autrement dit, notre en-tête sera le suivant.

struct bloc
{
    size_t taille;
    struct bloc *suivant;
};

Lors de la libération du bloc, il nous suffit d’employer le champ suivant de l’en-tête pour ajouter le bloc à la liste des blocs libres.

       En-tête                                          En-tête
<------------------->                            <------------------->
+---------+---------+-----------------+          +---------+---------+-----------------+
| Taille  | Suivant |   Bloc alloué   |          | Taille  | Suivant |   Bloc alloué   |
+---------+---------+-----------------+          +---------+---------+-----------------+
               +                                 ^
               |                                 |
               +---------------------------------+

Étant donné que l’en-tête précède le bloc alloué, il est impératif que sa taille soit « arrondie » de sorte que les données qui seront stockées dans le bloc respectent l’alignement le plus strict.

Avec ceci, vous devriez pouvoir réaliser une fonction static_free() et modifier la fonction static_malloc() en conséquence. ;)

Correction

Afficher/Masquer le contenu masqué
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#define MEM_MAX (1024UL * 1024UL)
#define ALIGN_MAX (_Alignof(max_align_t))
#define MULTIPLE_ALIGN_MAX(a) (size_t)((((a) % ALIGN_MAX) == 0) ? (a) : (a) + ALIGN_MAX - (a) % ALIGN_MAX)
#define TAILLE_EN_TETE MULTIPLE_ALIGN_MAX(sizeof (struct bloc))
struct bloc
{
    size_t taille;
    struct bloc *suivant;
};
static _Alignas(max_align_t) char mem[MEM_MAX];
static struct bloc *libre;
static void bloc_init(struct bloc *, size_t);
static size_t calcule_multiple_align_max(size_t);
static struct bloc *recherche_bloc_libre(size_t);
static void static_free(void *);
static void *static_malloc(size_t);
static void bloc_init(struct bloc *b, size_t taille)
{
    /*
     * Initialise les membres d'une structure `bloc`.
     */
    b->taille = taille;
    b->suivant = NULL;
}
static size_t calcule_multiple_align_max(size_t a)
{
    /*
     * Calcule le plus petit multiple de l'alignement maximal égal ou supérieur à `a`.
     */
    return (a % ALIGN_MAX == 0) ? a : a + ALIGN_MAX - (a % ALIGN_MAX);
}
static struct bloc *recherche_bloc_libre(size_t taille)
{
    /*
     * Recherche un bloc libre ou une suite de bloc libres dont la taille
     * est au moins égale à `taille`.
     */
    struct bloc *bloc = libre;
    struct bloc *precedent = NULL;
    struct bloc *ret = NULL;
    while (bloc != NULL)
    {
        if (bloc->taille >= taille)
        {
            if (precedent != NULL)
                precedent->suivant = bloc->suivant;
            else
                libre = bloc->suivant;
            ret = bloc;
            break;
        }
        precedent = bloc;
        bloc = bloc->suivant;
    }
    return ret;
}
static void *static_malloc(size_t taille)
{
    /*
     * Alloue un bloc de mémoire au moins de taille `taille`.
     */
    static size_t alloue = 0UL;
    void *ret;
    assert(taille > 0);
    taille = calcule_multiple_align_max(taille);
    ret = recherche_bloc_libre(taille);
    if (ret == NULL)
    {
        if (MEM_MAX - alloue < TAILLE_EN_TETE + taille)
        {
            fprintf(stderr, "Il n'y a plus assez de mémoire disponible.\n");
            return NULL;
        }
        ret = &mem[alloue];
        alloue += TAILLE_EN_TETE + taille;
        bloc_init(ret, taille);
    }
    return (char *)ret + TAILLE_EN_TETE;
}
static void static_free(void *ptr)
{
    /*
     * Ajoute le bloc fourni à la liste des blocs libres.
     */
    struct bloc *bloc = libre;
    struct bloc *nouveau;
    if (ptr == NULL)
         return;
    nouveau = (struct bloc *)((char *)ptr - TAILLE_EN_TETE);
    while (bloc != NULL && bloc->suivant != NULL)
        bloc = bloc->suivant;
    if (bloc == NULL)
        libre = nouveau;
    else
        bloc->suivant = nouveau;
}

Désormais, la fonction static_malloc() fait d’abord appel à la fonction recherche_bloc_libre() avant d’allouer de la mémoire de la réserve. Comme son nom l’indique, cette fonction parcourt la liste des blocs libres à la recherche d’un bloc d’une taille égale ou supérieure à celle demandée. Cette liste est matérialisée par la variable globale libre qui est un pointeur sur une structure de type bloc . Cette variable correspond au premier maillon de la liste et est employée pour initialiser le parcours.

Si aucun bloc libre n’est trouvé, alors la fonction static_malloc() pioche dans la réserve un bloc de la taille demandée augmentée de la taille de l’en-tête. Ces deux tailles sont « arrondies » au multiple égal ou directement supérieur de l’alignement le plus contraignant. Étant donné que la taille de l’en-tête est fixe, nous avons représenté sa taille « arrondie » à l’aide de la macroconstante TAILLE_EN_TETE et de la macrofonction MULTIPLE_ALIGN_MAX (qui réalise le même calcule que la fonction calcule_multiple_align_max() . Une fois le tout alloué, l’en-tête est « retiré » en ajoutant la taille de l’en-tête au pointeur référencant la mémoire allouée et le bloc demandé est retourné.

La fonction static_free() se rend à la fin de la liste et y ajoute le bloc qui lui est fourni en argument. Pour ce faire, elle « récupère » l’en-tête en soustrayant la taille de l’en-tête au pointeur fourni en argument. Notez également que cette fonction n’effectue aucune opération dans le cas où un pointeur nul lui est fourni (tout comme la fonction free() standard), ce qui peut être intéressant pour simplifier la gestion d’erreur dans certains cas.

Troisième étape : fragmentation et défragmentation

Bien, notre allocateur recycle à présent les blocs qu’il a précédemment alloués, c’est une bonne chose. Toutefois, un problème subsiste : la fragmentation de la mémoire allouée, autrement dit sa division en une multitude de petits blocs.

S’il s’agit d’un effet partiellement voulu (nous allouons par petits blocs pour préserver la réserve), il peut avoir des conséquences fâcheuses non désirées. En effet, imaginez que nous ayons alloué toute la mémoire sous forme de blocs de 16, 32 et 64 multiplets, si même tous ces blocs sont libres, notre allocateur retournera un pointeur nul dans le cas d’une demande de par exemple 80 multiplets… Voilà qui est plutôt gênant.

Une solution consiste à défragmenter la liste des blocs libres, c’est-à-dire fusionner plusieurs blocs pour en reconstruire d’autre avec une taille plus importante. Dans notre cas, nous allons mettre en œuvre ce système lors de la recherche d’un bloc libre : désormais, nous allons regarder si un bloc est d’une taille suffisante ou si plusieurs blocs, une fois fusionnés, seront de taille suffisante.

Fusion de blocs

Toutefois, une fusion de blocs n’est possible que si ceux-ci sont adjacents, c’est-à-dire s’ils se suivent en mémoire. Plus précisément, l’adresse suivant le premier bloc à fusionner doit être celle de début du second (autrement dit (char *)ptr_bloc1 + taille_bloc1 == (char *)ptr_bloc2 ).

Néanmoins, il ne nous est pas possible de vérifier cela facilement si notre liste de blocs libres n’est pas un minimum triée. En effet, sans tri, il nous serait nécessaire de parcourir toute la liste à la recherche d’éventuels blocs adjacents au premier, puis, de faire de même pour le deuxième et ainsi de suite, ce qui n’est pas particulièrement efficace.

À la place, il nous est possible de trier notre liste par adresses croissantes (ou décroissantes, le résultat sera le même) de sorte que si un bloc n’est pas adjacent au suivant, la recherche peut être immédiatement arrêtée pour ce bloc ainsi que tous ceux qui lui étaient adjacents. Ce tri peut être réalisé simplement lors de l’insertion d’un nouveau bloc libre en plaçant celui-ci correctement dans la liste à l’aide de comparaisons : s’il a une adresse inférieure à celle d’un élément de la liste, il est placé avant cet élément, sinon, le parcours continue.

En effet, deux pointeurs peuvent tout à fait être comparés du moment que ceux-ci référencent un même objet ou un même agrégat (c’est notre cas ici puisqu’ils référenceront tous des éléments du tableau mem ) et qu’ils sont du même type (une conversion explicite vers le type pointeur sur char sera donc nécessaire comme explicité auparavant).

N’oubliez pas que si deux ou plusieurs blocs sont fusionnés, il n’y a plus besoin que d’un seul en-tête, les autres peuvent donc être comptés comme de la mémoire utilisable.

Correction

Afficher/Masquer le contenu masqué
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#define MEM_MAX (1024UL * 1024UL)
#define ALIGN_MAX (_Alignof(max_align_t))
#define MULTIPLE_ALIGN_MAX(a) (size_t)((((a) % ALIGN_MAX) == 0) ? (a) : (a) + ALIGN_MAX - (a) % ALIGN_MAX)
#define TAILLE_EN_TETE MULTIPLE_ALIGN_MAX(sizeof (struct bloc))
struct bloc
{
    size_t taille;
    struct bloc *suivant;
};
struct suite
{
    struct bloc *precedent;
    struct bloc *premier;
    struct bloc *dernier;
    size_t taille;
};
static _Alignas(max_align_t) char mem[MEM_MAX];
static struct bloc *libre;
static void bloc_init(struct bloc *, size_t);
static size_t calcule_multiple_align_max(size_t);
static struct bloc *recherche_bloc_libre(size_t);
static void static_free(void *);
static void *static_malloc(size_t);
static void bloc_init(struct bloc *b, size_t taille)
{
    /*
     * Initialise les membres d'une structure `bloc`.
     */
    b->taille = taille;
    b->suivant = NULL;
}
static size_t calcule_multiple_align_max(size_t a)
{
    /*
     * Calcule le plus petit multiple de l'alignement maximal égal ou supérieur à `a`.
     */
    return (a % ALIGN_MAX == 0) ? a : a + ALIGN_MAX - (a % ALIGN_MAX);
}
static struct bloc *recherche_bloc_libre(size_t taille)
{
    /*
     * Recherche un bloc libre ou une suite de bloc libres dont la taille
     * est au moins égale à `taille`.
     */
    struct suite suite = { 0 };
    struct bloc *bloc = libre;
    struct bloc *precedent = NULL;
    struct bloc *ret = NULL;
    while (bloc != NULL)
    {
        if (bloc->taille >= taille)
        {
            if (precedent != NULL)
                precedent->suivant = bloc->suivant;
            else
                libre = bloc->suivant;
            ret = bloc;
            break;
        }
        else if (suite.dernier != NULL && (char *)suite.dernier + TAILLE_EN_TETE \
        + suite.dernier->taille == (char *)bloc)
        {
            suite.dernier = bloc;
            suite.taille += TAILLE_EN_TETE + bloc->taille;
        }
        else
        {
            suite.precedent = precedent;
            suite.premier = bloc;
            suite.dernier = bloc;
            suite.taille = bloc->taille;
        }
        if (suite.taille >= taille)
        {
            if (suite.precedent != NULL)
                suite.precedent->suivant = suite.dernier->suivant;
            else
                libre = suite.dernier->suivant;
            suite.premier->taille = suite.taille;
            ret = suite.premier;
            break;
        }
        precedent = bloc;
        bloc = bloc->suivant;
    }
    return ret;
}
static void *static_malloc(size_t taille)
{
    /*
     * Alloue un bloc de mémoire au moins de taille `taille`.
     */
    static size_t alloue = 0UL;
    void *ret;
    assert(taille > 0);
    taille = calcule_multiple_align_max(taille);
    ret = recherche_bloc_libre(taille);
    if (ret == NULL)
    {
        if (MEM_MAX - alloue < TAILLE_EN_TETE + taille)
        {
            fprintf(stderr, "Il n'y a plus assez de mémoire disponible.\n");
            return NULL;
        }
        ret = &mem[alloue];
        alloue += TAILLE_EN_TETE + taille;
        bloc_init(ret, taille);
    }
    return (char *)ret + TAILLE_EN_TETE;
}
static void static_free(void *ptr)
{
    /*
     * Ajoute le bloc fourni à la liste des blocs libres.
     */
    struct bloc *bloc = libre;
    struct bloc *precedent = NULL;
    struct bloc *nouveau;
    if (ptr == NULL)
         return;
    nouveau = (struct bloc *)((char *)ptr - TAILLE_EN_TETE);
    while (bloc != NULL)
    {
        if (nouveau < bloc)
        {
            if (precedent != NULL)
                precedent->suivant = nouveau;
            else
                libre = nouveau;
            nouveau->suivant = bloc;
            break;
        }
        precedent = bloc;
        bloc = bloc->suivant;
    }
    if (bloc == NULL)
        libre = nouveau;
}

La fonction static_free() a été aménagée afin que les adresses des différents blocs soient triées par ordre croissant. Si le nouveau bloc libéré a une adresse inférieure à un autre bloc, il est placé avant lui, sinon il est placé à la fin de la liste.

Quant à la fonction recherche_bloc_libre() , elle emploie désormais une structure suite qui comprend un pointeur vers le bloc précédent le début de la suite (champ precedent ), un pointeur vers le premier bloc de la suite (champ premier ), un pointeur vers le dernier bloc de la suite (champ dernier ) et la taille totale de la suite (champ taille ).

Dans le cas où cette structure n’a pas encore été modifiée ou que le dernier bloc de la suite n’est pas adjacent à celui qui le suit, le premier et le dernier blocs de la suite deviennent le bloc courant, la taille est réinitialisée à la taille du bloc courant et le bloc précédent la suite est… celui qui précède le bloc courant (ou un pointeur nul s’il n’y en a pas).

Si la suite atteint une taille suffisante, alors les blocs la composant sont fusionnés et le nouveau bloc ainsi construit est retourné.

Ce chapitre clôt cette troisième partie ainsi que ce cours. N’hésitez pas à revoir l’un ou l’autre passage qui vous ont semblé difficiles ou flous avant de prendre le large vers de nouveaux horizons. ;)


Cours GTK 2

Vu dans ... https://gtk.developpez.com/cours/gtk2

XXIV. Copyright

Copyright (c) 2002-2003 Equipe GTK-FR.

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.1 or any later version published by the Free Software Foundation; with no Invariant Sections, no Front-Cover Texts and no Back-Cover Texts. A copy of the license is included in the section entitled « Licence ».


Cours GTK 2

Vu dans ... https://gtk.developpez.com/cours/gtk2

Table des matières

  • I. Introduction
    • I-A. Qu'est-ce que GTK+ ?
    • I-B. Objectif du cours
    • I-C. À qui s'adresse ce cours ?
    • I-D. Organisation du cours
    • I-E. Comment y contribuer ?
    • I-F. Auteurs de ce cours

    • I. Introduction

    I-A. Qu'est-ce que GTK+ ?

    GTK+ est une bibliothèque permettant de créer des interfaces graphiques (GUI) très facilement.

    À l'origine, GTK+ a été développé pour les besoins du logiciel de traitement d'images GIMP (GNU Image Manipulation Program). Mais aujourd'hui, le domaine d'application ne se limite plus seulement à GIMP, mais est utilisée dans d'autres projets. Ainsi, l'environnement GNOME (GNU Network Object Model Environment) est basé sur GTK+.

    L'utilisation de GTK+ pour la création de GUI est très intéressante sur plusieurs points :

    • GTK+ est sous licence GNU LGPL. Cela fait de GTK+ une bibliothèque libre, permettant ainsi de l'utiliser ou de la modifier sans aucune contrainte financière. Pour avoir plus de renseignement, le plus simple est de visiter le site du projet GNU ;
    • GTK+ existe sur plusieurs plates-formes. En effet, GTK+ fonctionne sur les plates-formes UNIX-like, Windows, BeOs;
    • GTK+ est utilisable avec plusieurs langages de programmation. Même si les créateurs de GTK+ ont écris cette bibliothèque en C, sa structure orientée objets et sa licence ont permis à d'autres développeurs d'adapter GTK+ à leur langage préféré. Ainsi, il est maintenant possible de programmer des GUI GTK+ en C, C++, Ada, Perl, Python, PHP et bien d'autres.

    Les créateurs de GTK+ sont :

    • Peter Mattis ;
    • Spencer Kimball ;
    • Josh MacDonald.

    Actuellement, GTK+ est maintenu par :

    • Owen Taylor ;
    • Tim Janik.

    Le site officiel de GTK+ est http://www.gtk.org.

    I-B. Objectif du cours

    L'objectif de ce cours est de vous offrir un support en français pour la création de vos applications GTK+ en langage C. Ce cours développera en détail la majorité des fonctions de GTK+ tout en fournissant des exemples concrets. De ce fait, ce cours sera une sorte de tutoriel couplé à un manuel de référence complet.

    I-C. À qui s'adresse ce cours ?

    Ce cours est destiné plus particulièrement à trois types de programmeurs :

    • Les novices en programmation GUI ;
    • Les personnes connaissant d'autres GUI (API Win32, wxWindow) ;
    • Les personnes connaissant GTK+ 1.2.

    Pour profiter pleinement de ce cours, vous devez avoir une connaissance du langage C. Si tel n'est pas le cas, nous vous conseillons :

    • « Le langage C, norme ANSI » de Brian W. Kernighan et Denis M. Ritchie, édition DUNOD (ISBN : 2100051164). C'est le livre de référence du langage C, écrit par ses créateurs.
    • Le cours de Christian Casteyde : cours de C/C++. Il s'agit d'un très bon cours sur le C/C++. Même si ce cours est plutôt axé sur le C++, les premiers chapitres pourront vous apprendre beaucoup sur le langage C.

    I-D. Organisation du cours

    Le cours est divisé en deux parties :

    • La première section est consacrée à l'étude de la GLib qui est utilisée par GTK+;
    • La deuxième partie présentera les différents objets proposés par GTK+.

    Chaque partie sera ensuite divisée en plusieurs chapitres contenant un ou plusieurs objectifs. Chaque objectif sera, la plupart du temps, accompagné d'un programme exemple qui illustrera les notions abordées.

    Chaque chapitre, se terminera par la section En savoir plus, qui présente plus ou moins en détail, les fonctions et propriétés de l'objet étudié.


Cours GTK 2

Table des matières

  • II. Notre premier programme
    • II-A. 1. Installation de GTK+.
    • II-B. Configuration de votre compilateur
    • II-C. 3. Créer une application GTK+

    • II. Notre premier programme

    II-A. 1. Installation de GTK+.

    Avant toute chose, il faut savoir que GTK+ s'appuie sur plusieurs bibliothèques et que donc, il faut absolument les installer avant de pouvoir utiliser GTK+. Voici les différentes méthodes d'installation suivant l'outil avec lequel vous travaillez :

    • Sous Linux, votre distribution inclue une version de GTK+. Seules les distributions les plus récentes proposent la version 2 de GTK+. La première étape consiste donc à vérifier quelle version est installée sur votre système à l'aide de la ligne de commande suivante : 'pkg-config --modversion gtk+-2.0'. Si vous possédez déjà la version 2 de GTK+, vérifiez qu'une version plus récente que la la vôtre n'existe pas sur le site officiel de GTK+. Il vous faudra donc télécharger les fichiers sources de GTK+ sur le ftp officiel : ftp://ftp.gtk.org/pub/gtk/. Il ne vous restera plus qu'à suivre les instructions d'installation fournies avec les sources qui consistent, en général, à entrer les commandes suivantes : ./configure, make et make install .

    • Sous Windows :

      • Avec Dev-Cpp, il suffit de télécharger les packages imagelib , GTK+ 2.0.0 runtime libraries et GTK+ 2.0.0 development package à l'adresse suivante : http://devpaks.org/category.php?category=GTK. Ensuite, l'installation de ces packages doit s'effectuer dans le même ordre que précédemment. Cette solution est, sous Windows, la plus simple et la moins chère puisque Dev-Cpp est gratuit. L'environnement de développement et le compilateur sont disponible en téléchargement à cette adresse : http://www.bloodshed.net/dev/devcpp.html.

      • Avec Visual C++, le plus rapide est de récupérer tous les fichiers nécessaires sur le site de Tor Lillqvist. Il s'occupe du portage de GTK+ sur les plates-formes Win32 et propose les fichiers nécessaires à la création et à l'exécution de programmes GTK+. Les fichiers à télécharger sont : libiconv, libintl, dirent, zlib, libpng, libjpeg, libtiff, freetype2, glib, atk, pango et gtk. La dernière étape consiste à décompresser tous les fichiers dans un répertoire de votre choix (par exemple C:\GTK) et à ajouter ce répertoire à la variable d'environnement PATH de Windows. Pour cela, il suffit d'aller dans le panneau de configuration, de cliquer sur l'icône Système . Dans la boîte de dialogue, allez dans l'onglet Avancé et ajoutez le chemin et rebootez votre ordinateur.

    II-B. Configuration de votre compilateur

    Cette étape est obligatoire à la création d'un exécutable GTK+. Elle consiste surtout à configurer l'éditeur de lien et intervient à de moments différents suivants votre outil de travail :

    • Avec gcc, il faut rajouter l'option `pkg-config --cflags --libs gtk+-2.0` à votre ligne de compilation ;
    • Avec Dev-Cpp, c'est la création du projet qui est importante. Dans la boîte de dialogue « New Project », sélectionner l'onglet GUI puis GTK+ et C Project ;
    • Avec Visual C++, il faut dans un premier temps, créer un nouveau projet Console . L'étape suivante consiste à modifier les propriétés du projet en ajoutant les librairies suivantes en entrée de l'éditeur de lien : gtk-win32-2.0.lib , gobject-2.0.lib et glib-2.0.lib .

    Vous êtes maintenant prêt à créer votre première application GTK+.

    II-C. 3. Créer une application GTK+

    La première chose à faire est d'ajouter le fichier en-tête à votre code source :

    #include <gtk/gtk.h>

    Et ensuite, dans votre fonction main, il faut initialiser GTK+ avec cette fonction :

    void gtk_init(int *argc, char ***argv);

    Les deux paramètres à passer à cette fonction correspondent aux paramètres reçus par la fonction main. Cette fonction récupère les valeurs passées par la ligne de commande et configure GTK+. Les différentes valeurs qui sont reconnues par GTK+ sont :

    • --gtk-module ;
    • --g-fatal-warnings ;
    • --gtk-debug ;
    • --gtk-no-debug ;
    • --gdk-debug ;
    • --gdk-no-debug ;
    • --display ;
    • --sync ;
    • --name ;
    • --class.

    Voici le code source complet de notre première application. Elle ne fait strictement rien, mais il s'agit bien d'une application GTK+ :

    #include <stdlib.h>
#include <gtk/gtk.h>
int main(int argc, char **argv)
{
    /* Initialisation de GTK+ */
    gtk_init(&argc, &argv);
    return EXIT_SUCCESS;
}


Cours GTK 2

Table des matières

  • III. Les fenêtres
    • III-A. Créer et afficher une fenêtre.
      • III-A-1. Création d'une fenêtre
      • III-A-2. Affichage de la fenêtre.
      • III-A-3. Les signaux.
      • III-A-4. Exemple
    • III-B. Personnaliser une fenêtre
      • III-B-1. Position de la fenêtre
      • III-B-2. Titre de la fenêtre.
      • III-B-3. Taille de la fenêtre
      • III-B-4. Programme exemple
    • III-C. En savoir plus
      • III-C-1. Signaux
      • III-C-2. Fonctions documentées
      • III-C-3. Fonctions non documentées

    • III. Les fenêtres

    Le premier élément d'une interface graphique est bien entendu la fenêtre. Il est donc tout à fait normal que ce soit le premier objet étudié dans le cadre de ce cours.

    III-A. Créer et afficher une fenêtre.

    Avant de commencer l'étude des fenêtres, il faut savoir que les objets graphiques de GTK+ sont appelés des widgets . Un widget est en fait une structure définissant les propriétés d'un objet associée à une large panoplie de fonctions permettant de manipuler ces objets.

    Ici, le terme est à prendre au sens littéral, mais aussi au sens Programmation Orientée Objet (POO). En effet, bien que GTK+ soit écrit en C, il introduit la notion d'héritage et les widgets de GTK+ suivent une hiérarchie bien définie. Ainsi tous les objets graphiques héritent des propriétés et des fonctions d'un widget de base qui s'appelle GtkWidget.

    Ainsi le widget permettant d'afficher une fenêtre se nomme GtkWindow. Il a bien sûr ses propres fonctions, mais grâce à l'héritage il bénéficie aussi des fonctions de plusieurs autres widgets. Voici donc la position de GtkWindow dans la hiérarchie GTK+ :

    III-A-1. Création d'une fenêtre

    Dans un premier temps, il faut déclarer un pointeur sur notre objet fenêtre. Bien que nous voulions créer un objet GtkWindow, il faut déclarer un objet GtkWidget.

    GtkWidget *pWindow;

    Par la suite, quel que soit l'objet à créer, il faudra toujours déclarer un GtkWidget.

    Une fois l'objet pWindow déclarer, il faut l'initialiser. Pour cela, une fonction est à notre disposition :

    GtkWidget* gtk_window_new(GtkWindowType type);

    Le paramètre type définit le type de la fenêtre en cours de création, et peut prendre une des deux valeurs suivantes :

    • GTK_WINDOW_TOPLEVEL : pour créer une fenêtre complète avec une zone réservée dans la barre des tâches ;
    • GTK_WINDOW_POPUP : pour créer une fenêtre sans aucune décoration (barre de titre, bordure…).

    La valeur de retour est le pointeur sur notre nouvel objet fenêtre. Cette fonction est donc à utiliser comme ceci :

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    La création de la fenêtre est maintenant terminée.

    III-A-2. Affichage de la fenêtre.

    Un widget n'a pas de fonction spécifique liée à son affichage, mais utilise une fonction de GtkWidget qui est :

    void gtk_widget_show(GtkWidget *widget);

    Il suffit donc de passer en paramètre le widget que nous voulons afficher, ainsi pour afficher notre fenêtre, la ligne de code est :

    gtk_widget_show(pWindow);

    Au contraire, pour détruire la fenêtre, la fonction sera :

    void gtk_widget_destroy(GtkWidget *widget);

    Voici donc le code source complet de notre programme affichant une fenêtre.

    #include <stdlib.h>
#include <gtk/gtk.h>
int main(int argc,char **argv)
{
    /* Declaration du widget */
    GtkWidget *pWindow;
    gtk_init(&argc,&argv);

    /* Création de la fenêtre */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    /* Affichage de la fenêtre */
    gtk_widget_show(pWindow);
    /* Destruction de la fenêtre */
    gtk_widget_destroy(pWindow);

    return EXIT_SUCCESS;
}

    En essayant ce programme, vous ne verrez sûrement pas la fenêtre s'afficher. La raison est simple : la destruction de la fenêtre a lieu tout de suite après son affichage. Nous allons maintenant étudier la théorie de signaux afin que l'application ne se termine qu'au moment où l'utilisateur le demande.

    III-A-3. Les signaux.

    Nous allons maintenant étudier comment faire réagir une application GTK+ aux actions de l'utilisateur.

    Dans un premier temps, lorsque l'utilisateur interagi avec l'application (clique sur un bouton, ferme une fenêtre…), le widget concerné émet un signal (par exemple « destroy » lorsque l'on ferme une fenêtre). Chaque widget est associé à un ou plusieurs signaux et permet donc ainsi de programmer toutes les actions possibles.

    Ce signal va ensuite être intercepté par une boucle événementielle qui va vérifier qu'une action spécifique à ce signal et ce widget a bien été définie. Si tel est le cas, la fonction associée sera exécutée. Ce type de fonction s'appelle fonction callback .

    Il faut donc créer une fonction callback pour chacun des événements susceptible d'avoir lieu pendant l'exécution du programme et associer (ou connecter) cette fonction à un signal.

    La première étape consiste donc à créer une fonction callback. Dans la majorité des cas, une fonction callback sera de cette forme :

    void nom_de_la_fonction(GtkWidget *widget, gpointer data)

    Le paramètre widget est le widget qui a émis le signal, et data est une donnée supplémentaire qui peut être utilisée.

    Ensuite, pour connecter un signal à une fonction callback, GTK+ utilise une fonction de la bibliothèque GLib qui est :

    gulong g_signal_connect(gpointer *object, const gchar *name, GCallback func, gpointer func_data );

    Le premier paramètre object , correspond au widget qui émet le signal. Cependant, la variable demandée par g_signal_connect étant de type gpointer* (correspond à void* du C standard) et le widget de type GtkWidget* , il faut convertir ce dernier pour ne pas provoquer d'erreur lors de la compilation. Pour cela GTK+ (ou dans ce cas GLib) fourni une macro de conversion (G_OBJECT) qui sera à utiliser à chaque utilisation de cette fonction.

    Le second paramètre name , est le signal qui doit être intercepté par la boucle évènementielle. Dans ce cours, certains signaux seront utilisés dans les exemples, mais la rubrique « En savoir plus » donnera une liste complète des signaux qui peuvent être émis par un widget.

    Le troisième paramètre func , définit la fonction callback à associer au signal. Cette fois encore, il faudra utiliser une macro de conversion qui est G_CALLBACK(func) .

    Le dernier paramètre func_data , permet de spécifier une donnée quelconque à laquelle la fonction callback peut avoir accès pour effectuer son travail correctement.

    Une fois que les signaux sont connectés, il faut lancer la boucle événementielle en appelant cette fonction :

    void gtk_main(void);

    Cela aura pour effet de faire entrer GTK+ dans une boucle infinie qui ne pourra être stoppée que par l'appel de la fonction de fin de boucle qui est :

    void gtk_main_quit(void);

    Ces fonctions correspondent au minimum afin de pouvoir créer une application GTK+ correcte. D'autres fonctions permettent une utilisation avancée des signaux et de la boucle événementielle et seront traitées dans un autre chapitre du cours.

    III-A-4. Exemple

    Nous allons maintenant faire en sorte que la fenêtre crée reste visible jusqu'à que l'utilisateur clique sur la croix située à droite de la barre de titre.

    La première étape est la déclaration de la fonction callback :

    void OnDestroy(GtkWidget *pWidget, gpointer pData);

    La seule action que doit faire cette fonction est d'arrêter la boucle événementielle. La seule instruction à ajouter est donc gtk_main_quit(); .

    Il faut ensuite connecter le signal « destroy » de la fenêtre à cette fonction :

    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(OnDestroy), NULL);

    Et pour terminer, il faut lancer la boucler événementielle.

    gtk_main();

    Voici donc le code source complet de notre application affichant une fenêtre.

    #include <stdlib.h>
#include <gtk/gtk.h>
void OnDestroy(GtkWidget *pWidget, gpointer pData);
int main(int argc,char **argv)
{
    /* Déclaration du widget */
    GtkWidget *pWindow;
    gtk_init(&argc,&argv);

    /* Création de la fenêtre */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    /* Connexion du signal "destroy" */
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(OnDestroy), NULL);
    /* Affichage de la fenêtre */
    gtk_widget_show(pWindow);
    /* Demarrage de la boucle evenementielle */
    gtk_main();

    return EXIT_SUCCESS;
}
void OnDestroy(GtkWidget *pWidget, gpointer pData)
{
    /* Arret de la boucle evenementielle */
    gtk_main_quit();
}

    Résultat :

    Image non disponible

    III-B. Personnaliser une fenêtre

    Nous allons maintenant étudier quelques fonctions permettant de modifier l'aspect de la fenêtre précédemment créée. L'exemple final consistera à créer une fenêtre de taille 320x200 au centre de l'écran ayant pour titre « Chapitre I. »

    III-B-1. Position de la fenêtre

    La première fonction étudiée permet de définir la position de la fenêtre avant son affichage. Son prototype est le suivant :

    void gtk_window_set_position(GtkWindow* window, GtkWindowPosition position);

    Le premier paramètre correspond à la fenêtre dont on veut spécifier la position. Ce paramètre étant de type GtkWindow* , il faudra convertir la fenêtre (de type GtkWidget* ) avec la macro GTK_WINDOW .

    Le deuxième paramètre est la position que l'on veut donner à la fenêtre. Les valeurs acceptées sont :

    • GTK_WIN_POS_NONE : la fenêtre aura une position aléatoire lors de son affichage ;
    • GTK_WIN_POS_CENTER : la fenêtre sera centrée à l'écran ;
    • GTK_WIN_POS_MOUSE : le coin supérieur droit de la fenêtre correspondra à la position de la souris au moment de l'affichage ;
    • GTK_WIN_POS_CENTER_ALWAYS : la fenêtre sera centrée et ne pourra être déplacée ;
    • GTK_WIN_POS_CENTER_ON_PARENT : la fenêtre sera centrée par rapport à la fenêtre parente (notion abordée dans le prochain chapitre).

    La deuxième fonction est utilisable à tout moment du programme et permet de donner la position exacte de la fenêtre :

    void gtk_window_move(GtkWindow *window, gint x, gint y);

    Le premier paramètre est toujours la fenêtre dont on veut modifier la position.

    Les deux autres paramètres sont la nouvelle position de la fenêtre. Le paramètre x est la position suivante l'axe X (axe horizontal) et le paramètre y , la position suivant l'axe Y (axe vertical). Le type gint est le type int du C.

    À l'inverse, pour connaître la position de la fenêtre, il faut utiliser cette fonction :

    void gtk_window_get_position(GtkWindow *window, gint *root_x, gint *root_y);

    Les paramètres correspondent aux mêmes valeurs que pour la fonction gtk_window_move .

    Pour le programme exemple, le plus simple est d'utiliser la première fonction ainsi :

    gtk_window_set_position(GTK_WINDOW(pWindow), GTK_WIN_POS_CENTER);

    III-B-2. Titre de la fenêtre.

    Cette fois-ci, une seule fonction est à notre disposition pour modifier le titre de la fenêtre. Cette dernière est très simple d'utilisation :

    void gtk_window_set_title(GtkWindow* window, const gchar* title)

    Le deuxième paramètre title est bien entendu le titre que l'on veut donner à la fenêtre ( gchar correspond à char en C).

    Pour l'exemple, la ligne de code sera :

    gtk_window_set_title(GTK_WINDOW(pWindow), "Chapitre I.");

    Pour connaître le titre d'une fenêtre, la fonction est :

    G_CONST_RETURN gchar* gtk_window_get_title(GtkWindow *window);

    La variable qui recevra la valeur de retour de cette fonction devra être de type const gchar* .

    III-B-3. Taille de la fenêtre

    La fonction pour définir la taille par défaut de la fenêtre est :

    void gtk_window_set_default_size(GtkWindow* window, gint width, gint height);

    Le paramètre width est la largeur de la fenêtre tandis que le paramètre height est sa hauteur.

    Pour notre exemple, la ligne de code sera :

    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);

    Et très logiquement, la fonction pour connaître la taille par défaut de la fenêtre est :

    void gtk_window_get_default_size(GtkWindow *window, gint *width, gint *height);

    III-B-4. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>
void OnDestroy(GtkWidget *pWidget, gpointer pData);
int main(int argc,char **argv)
{
    /* Déclaration du widget */
    GtkWidget *pWindow;
    gtk_init(&argc,&argv);

    /* Création de la fenêtre */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    /* Définition de la position */
    gtk_window_set_position(GTK_WINDOW(pWindow), GTK_WIN_POS_CENTER);
    /* Définition de la taille de la fenêtre */
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    /* Titre de la fenêtre */
    gtk_window_set_title(GTK_WINDOW(pWindow), "Chapitre I.");
    /* Connexion du signal "destroy" */
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(OnDestroy), NULL);
    /* Affichage de la fenetre */
    gtk_widget_show(pWindow);
    /* Démarrage de la boucle événementielle */
    gtk_main();

    return EXIT_SUCCESS;
}
void OnDestroy(GtkWidget *pWidget, gpointer pData)
{
    /* Arrêt de la boucle événementielle */
    gtk_main_quit();
}

    Résultat :

    Image non disponible

    III-C. En savoir plus

    III-C-1. Signaux

    Prototypes fonctions callback :

    • activate-default

      • void user_function(GtkWindow *window, gpointer user_data);

    • activate-focus

      • void user_function(GtkWindow *window, gpointer user_data);

    • frame-event

      • gboolean user_function(GtkWindow *window, GdkEvent *event, gpointer user_data);

    • keys-changed

      • void user_function(GtkWindow *window, gpointer user_data);

    • move-focus

      • void user_function(GtkWindow *window, GtkDirectionType arg1, gpointer user_data);

    • set-focus

      • void user_function(GtkWindow *window, GtkWidget *widget, gpointer user_data);

    III-C-2. Fonctions documentées

    void gtk_window_set_resizable(GtkWindow *window, gboolean resizable);

    Permet de définir si l'utilisateur peut modifier la taille de la fenêtre.

    Entrée(s) :

    • window : la fenêtre.
    • resizable : TRUE si l'on peut modifier la taille, FALSE sinon.

    Sortie : rien.

    gboolean gtk_window_get_resizable(GtkWindow *window);

    Pour savoir si la taille de la fenêtre est modifiable par l'utilisateur.

    Entrée(s) :

    • window : la fenêtre.

    Sortie : TRUE si elle est modifiable, FALSE sinon.

    void gtk_window_set_gravity(GtkWindow *window, GdkGravity gravity);

    Permet de définir le point d'origine pour le placement de la fenêtre.

    Entrée(s) :

    • window : la fenêtre

    • gravity : le point d'origine. Valeurs possibles :

      • GDK_GRAVITY_NORTH_WEST origine en haut à gauche de l'écran ;
      • GDK_GRAVITY_NORTH origine en haut au milieu de l'écran ;
      • GDK_GRAVITY_NORTH_EAST origine en haut à droite de l'écran ;
      • GDK_GRAVITY_WEST origine au milieu à gauche de l'écran ;
      • GDK_GRAVITY_CENTER origine au milieu de l'écran ;
      • GDK_GRAVITY_EAST origine au milieu à droite de l'écran ;
      • GDK_GRAVITY_SOUTH_WEST origine en bas à gauche de l'écran ;
      • GDK_GRAVITY_SOUTH origine en bas au milieu de l'écran ;
      • GDK_GRAVITY_SOUTH_EAST origine en bas à droite de l'écran ;
      • GDK_GRAVITY_STATIC l'origine est le coin supérieur droit de la fenêtre elle-même.

    Sortie : rien.

    GdkGravity gtk_window_get_gravity(GtkWindow *window);

    Pour obtenir le point d'origine à l'écran.

    Entrée(s) :

    • window : la fenêtre.

    Sortie GdkGravity (énumérer plus haut).

    void gtk_window_set_focus(GtkWindow *window, GtkWidget *focus);

    Permet de définir le widget qui aura le focus.

    Entrée(s) :

    • window : la fenêtre contant le widget.
    • focus : le widget à sélectionner.

    Sortie : rien.

    GtkWidget* gtk_window_get_focus(GtkWindow *window);

    Pour connaître le widget qui a le focus.

    Entrée(s) :

    • window : la fenêtre.

    Sortie : un pointeur sur le widget ayant le focus.

    void gtk_window_set_default(GtkWindow *window, GtkWidget *default_widget);

    Pour définir le widget qui aura le focus par défaut.

    Entrée(s) :

    • window : la fenêtre contant le widget.
    • default_widget : le widget à sélectionner.

    Sortie : rien.

    void gtk_window_iconify(GtkWindow *window);

    Permet de minimiser la fenêtre.

    Entrée(s) :

    • window : la fenêtre.

    Sortie : rien.

    void gtk_window_deiconify(GtkWindow *window);

    Pour réafficher la fenêtre lorsque celle-ci est minimisée.

    Entrée(s) :

    • window : la fenêtre.

    Sortie : rien.

    void gtk_window_maximize(GtkWindow *window);

    Permet de maximiser la fenêtre.

    Entrée(s) :

    • window : la fenêtre.

    Sortie : rien.

    void gtk_window_unmaximize(GtkWindow *window);

    Redonne une fenêtre maximisée sa taille précédente.

    Entrée(s) :

    • window : la fenêtre.

    Sortie : rien.

    void gtk_window_set_decorated(GtkWindow *window, gboolean setting);

    Pour afficher la barre de titre de la fenêtre.

    Entrée(s) :

    • window : la fenêtre.
    • resizable :TRUE si l'on veut l'afficher, FALSE sinon.

    Sortie : rien.

    gboolean gtk_window_get_decorated(GtkWindow *window);

    Pour savoir si la barre de titre d'une fenêtre est visible.

    Entrée(s) :

    • window : la fenêtre.

    Sortie : TRUE si elle est visible, FALSE sinon.

    void gtk_window_resize(GtkWindow *window, gint width, gint height);

    Pour définir la nouvelle taille de la fenêtre.

    Entrée(s) :

    • window : la fenêtre.
    • width : largeur.
    • height : hauteur.

    Sortie : rien.

    III-C-3. Fonctions non documentées

    void gtk_window_set_wmclass(GtkWindow *window, const gchar *wmclass_name, const gchar *wmclass_class);
void gtk_window_set_policy(GtkWindow *window, gintallow_shrink, gintallow_grow, gintauto_shrink);
void gtk_window_add_accel_group(GtkWindow *window, GtkAccelGroup *accel_group);
void gtk_window_remove_accel_group(GtkWindow *window, GtkAccelGroup *accel_group);
gboolean gtk_window_activate_focus(GtkWindow *window);
gboolean gtk_window_activate_default(GtkWindow *window);
void gtk_window_set_modal(GtkWindow *window, gboolean modal);
void gtk_window_set_geometry_hints(GtkWindow *window, GtkWidget *geometry_widget, GdkGeometry *geometry, GdkWindowHints geom_mask);
void gtk_window_set_transient_for(GtkWindow *window, GtkWindow *parent);
void gtk_window_set_destroy_with_parent(GtkWindow *window, gboolean setting);
GList* gtk_window_list_toplevels(void);
void gtk_window_add_mnemonic(GtkWindow *window, guintkeyval, GtkWidget *target);
void gtk_window_remove_mnemonic(GtkWindow *window, guintkeyval, GtkWidget *target);
gboolean gtk_window_mnemonic_activate(GtkWindow *window, guintkeyval, GdkModifierType modifier);
void gtk_window_present(GtkWindow *window);
void gtk_window_stick(GtkWindow *window);
void gtk_window_unstick(GtkWindow *window);
void gtk_window_begin_resize_drag(GtkWindow *window, GdkWindowEdge edge, gint button, gintroot_x, gintroot_y, guint32 timestamp);
void gtk_window_begin_move_drag(GtkWindow *window, gintbutton, gintroot_x, gintroot_y, guint32 timestamp);
void gtk_window_set_frame_dimensions(GtkWindow *window, gintleft, ginttop, gintright, gintbottom);
void gtk_window_set_has_frame(GtkWindow *window, gboolean setting);
void gtk_window_set_mnemonic_modifier(GtkWindow *window, GdkModifierType modifier);
void gtk_window_set_role(GtkWindow *window, const gchar *role);
void gtk_window_set_type_hint(GtkWindow *window, GdkWindowTypeHinthint);
GList* gtk_window_get_default_icon_list(void);
gboolean gtk_window_get_destroy_with_parent(GtkWindow *window);
void gtk_window_get_frame_dimensions(GtkWindow *window, gint*left, gint*top, gint*right, gint*bottom);
gboolean gtk_window_get_has_frame(GtkWindow *window);
GdkPixbuf* gtk_window_get_icon(GtkWindow *window);
GList* gtk_window_get_icon_list(GtkWindow *window);
GdkModifierType gtk_window_get_mnemonic_modifier(GtkWindow *window);
gboolean gtk_window_get_modal(GtkWindow *window);
G_CONST_RETURN gchar* gtk_window_get_role(GtkWindow *window);
GtkWindow* gtk_window_get_transient_for(GtkWindow *window);
GdkWindowTypeHint gtk_window_get_type_hint(GtkWindow *window);
gboolean gtk_window_parse_geometry(GtkWindow *window, const gchar *geometry);
void gtk_window_reshow_with_initial_size(GtkWindow *window);
void gtk_window_set_default_icon_list(GList *list);
void gtk_window_set_icon(GtkWindow *window, GdkPixbuf *icon);
void gtk_window_set_icon_list(GtkWindow *window, GList *list);
void gtk_decorated_window_init(GtkWindow *window);
void gtk_decorated_window_calculate_frame_size(GtkWindow *window);
void gtk_decorated_window_set_title(GtkWindow *window, const gchar *title);
void gtk_decorated_window_move_resize_window(GtkWindow *window, gintx, ginty, gintwidth, gintheight);


Cours GTK 2

Table des matières

  • IV. Les labels
    • IV-A. Création et affichage d'un label.
      • IV-A-1. Création d'un label.
      • IV-A-2. Insérer le texte dans la fenêtre.
      • IV-A-3. Programme exemple
    • IV-B. Les caractères accentués
      • IV-B-1. Application test
      • IV-B-2. Comment ça marche ?
      • IV-B-3. Solution
      • IV-B-4. Programme exemple
    • IV-C. Formatage du texte
      • IV-C-1. Définition du format
      • IV-C-2. Les balises rapides
      • IV-C-3. La balise <span>
      • IV-C-4. Alignement du texte
      • IV-C-5. Programme exemple
    • IV-D. En savoir plus
      • IV-D-1. Fonctions documentées
      • IV-D-2. Fonctions non documentées

    • IV. Les labels

    Maintenant que nous savons créer une fenêtre, nous allons créer l'incontournable « Hello World ! ». Pour cela, nous allons utiliser le widget GtkLabel qui nous permet d'afficher du texte.

    Image non disponible

    IV-A. Création et affichage d'un label.

    Nous allons maintenant voir comment créer un label, et comment l'insérer dans la fenêtre principale.

    IV-A-1. Création d'un label.

    On crée un pointeur vers la structure GtkWidget.

    Dans un premier temps, il faut déclarer le pointeur sur l'objet GtkLabel :

    Sélectionnez
    GtkWidget *pLabel;

    Ensuite il faut initialiser cet objet. Pour cela, il n'existe qu'une seule fonction :

    Sélectionnez
    GtkWidget* gtk_label_new(const char* str);

    Le paramètre str n'est autre que la chaîne de caractères qu'il faudra afficher.

    Pour notre exemple, la ligne de code sera :

    Sélectionnez
    pLabel = gtk_label_new("Hello World!");

    IV-A-2. Insérer le texte dans la fenêtre.

    Pour pouvoir afficher le texte, il faut insérer le widget pLabel dans la fenêtre principale (widget pWindow). Pour cela, nous allons utiliser le fait que le widget GtkWindow dérive du widget GtkContainer. L'atout principal d'un GtkContainer est, comme son nom le laisse entendre, qu'il peut contenir et afficher un autre widget. Les widgets de ce type sont appelés des conteneurs. Il faudra donc différencier deux types d'objets :

    • Le widget conteneur (aussi appelé widget parent) ;
    • Le widget contenu (aussi appelé widget enfant) qui pourra par la suite devenir lui-même un conteneur s'il possède les propriétés de GtkContainer.

    Étudions donc maintenant la fonction qui nous servira à insérer un widget dans un widget conteneur :

    Sélectionnez
    void gtk_container_add(GtkContainer *container, GtkWidget *widget);

    Le premier paramètre, container, est le widget conteneur dans lequel on veut insérer le widget widget qui lui est passé en deuxième paramètre. Le premier paramètre de cette fonction est de type GtkContainer, alors que les widgets conteneurs seront tous de type GtkWidget. Il faudra donc à nouveau utiliser une macro de conversion qui cette fois est GTK_CONTAINER() .

    Pour notre exemple, la ligne de code sera :

    Sélectionnez
    gtk_container_add(GTK_CONTAINER(pWindow), pLabel);

    IV-A-3. Programme exemple

    Sélectionnez
    #include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc,char **argv)
{
    GtkWidget* pWindow;
    GtkWidget* pLabel;

    gtk_init(&argc,&argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "Les labels");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);

    /* Creation du label */
    pLabel=gtk_label_new("Hello World!");

    /* On ajoute le label a l'intérieur de la fenêtre */
    gtk_container_add(GTK_CONTAINER(pWindow), pLabel);

    /* Affichage de la fenêtré et de tout ce qu'il contient */
    gtk_widget_show_all(pWindow);

    /* Connexion du signal
    /* On appelle directement la fonction de sortie de boucle */
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    IV-B. Les caractères accentués

    Vous avez peut-être déjà essayé d'afficher du texte contenant des caractères tel que « é, è, à, … », et lors de l'exécution, votre texte ne s'affiche complètement. Nous allons maintenant voir le pourquoi du comment.

    IV-B-1. Application test

    Pour vous montrer tout cela, nous allons reprendre l'exemple précédent en remplaçant « Hello Word » par « Texte à afficher », et cela ne marche pas! En effet vous obtenez cela :

    Image non disponible

    Et en plus sur la console, vous avez le message d'erreur suivant :

    ** (Label2.exe) : WARNING **: Invalid UTF8 string passed to pango_layout_set_text()

    IV-B-2. Comment ça marche ?

    Pour afficher du texte, Gtk utilise la librairie Pango qui s'occupe du rendu et de l'affichage du texte. Le but de Pango est de permettre l'internationalisation des applications, et pour cela Pango utilise l'encodage UTF8. Ce charset est codée sur 16 bits, ce qui offre la possibilité d'afficher plus 65000 caractères, permettant ainsi d'afficher des caractères accentués et bien plus (ex : pour le grec, le chinois…).

    Puisque Pango sait faire tout cela, pourquoi le test n'a-t-il pas fonctionné? Et bien, tout simplement parce que votre système d'exploitation n'utilise pas ce charset. Lorsque vous souhaitez afficher « Texte à afficher » , Pango va avant tout vérifier l'encodage d'un caractère et si l'un de ces caractères n'est pas correct, Pango va arrêter son processus de rendu et envoyer un message d'erreur.

    Le moyen le plus simple pour voir les effets d'une erreur d'encodage est de changer celui de votre navigateur. Modifiez les options de votre navigateur afin d'utiliser l'encodage UNICODE . Cette page web utilisant le charset iso-8859-1 , les caractères accentués deviendront des carrés ou autre chose.

    IV-B-3. Solution

    Une seule chose s'impose à nous : il faut convertir notre chaîne de caractère en UTF8. Pour cela, il faut utiliser une fonction de Glib qui est :

    Sélectionnez
    gchar* g_locale_to_utf8(const gchar *opsysstring, gsize len, gsize *bytes_read, gsize *bytes_written, GError **error);

    La valeur de retour est le pointeur sur la chaîne de caractère fraîchement convertie. Si une erreur est survenue lors de la conversion, g_locale_to_utf8 renvoie NULL . Il faudra tout de même libérer le mémoire allouée par cette fonction lorsque la variable ne sera plus utile.

    Le premier paramètre opsysstring est la chaîne de caractère à convertir et le second paramètre len correspond à la longueur de la chaîne. Ce paramètre sera en général égal à -1 (on laisse la bibliothèque calculer la longueur toute seule).

    Les trois derniers paramètres sont utiles en cas d'erreur lors de la conversion. Tout d'abord bytes_read est le nombre d'octet qui ont été lus dans le texte à convertir, et bytes_writen le nombre d'octet qui ont été écris dans la nouvelle chaîne de caractères.

    Le dernier paramètre error , donne plus de précision en cas d'erreur. Voici la liste des messages d'erreur possibles :

    • G_CONVERT_ERROR_NO_CONVERSION ;
    • G_CONVERT_ERROR_ILLEGAL_SEQUENCE ;
    • G_CONVERT_ERROR_FAILED ;
    • G_CONVERT_ERROR_PARTIAL_INPUT ;
    • G_CONVERT_ERROR_BAD_URI ;
    • G_CONVERT_ERROR_NOT_ABSOLUTE_PATH.

    Pour notre exemple, nous n'allons pas utiliser les trois derniers paramètres. En cas d'erreur, il n'y aura donc aucun moyen d'avoir plus de précision sur l'erreur survenue.

    IV-B-4. Programme exemple

    Sélectionnez
    #include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc,char **argv)
{
    GtkWidget* pWindow;
    GtkWidget* pLabel;
    gchar* sUtf8;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "Les labels II");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);

    /* Creation du label avec     g_locale_to_utf8 */
    sUtf8 = g_locale_to_utf8("Texte à afficher", -1, NULL, NULL, NULL);
    pLabel=gtk_label_new(sUtf8);
    g_free(sUtf8);

    /* On ajoute le label a l'intérieur de la fenêtre */
    gtk_container_add(GTK_CONTAINER(pWindow), pLabel);

    /* Affichage de la fenêtre et de tout ce qu'il contient */
    gtk_widget_show_all(pWindow);

    /* Connexion du signal
    /* On appelle directement la fonction de sortie de boucle */
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    IV-C. Formatage du texte

    Nous allons maintenant voir comment modifier l'apparence de notre texte (police, couleur…).
    Pour notre application test, nous allons afficher une ligne en Courier gras taille 10, une ligne en Times New Roman italique bleu taille 12, et une ligne en Verdana souligné taille 16. Et tout cela centré (bien sûr).

    IV-C-1. Définition du format

    Une fois encore, nous allons utiliser les propriétés de Pango (même si nous n'allons pas utiliser les fonctions de Pango directement).

    Pour définir le format du texte, il suffit d'insérer des balises à l'intérieur même de notre texte. Pango s'occupe ensuite de rechercher les balises puis de formater le texte à notre convenance.

    IV-C-2. Les balises rapides

    Les balises rapides servent à mettre le texte en gras, italique ou autre de manière très simple. Voici l'intégralité de ces balises :

    Balise | Action | Exemple
<b> | Met le texte en gras | texte
<big> | Augment légèrement la taille du texte | texte
<i> | Met le texte en italique | texte
<s> | Barre le texte | texte
<sub> | Met le texte en indice | texte
<sup> | Met le texte en exposant | texte
<small> | Diminue légèrement la taille du texte | texte
<tt> | Met le texte en télétype | texte
<u> | Souligne le texte | texte

    Pour utiliser ces balises, il suffit d'encadrer le texte à modifier des balises de formatage. Par exemple, pour mettre le texte en gras, il faudra entrer : « Normal vs <b>Gras</b> ».

    Mais cela ne suffit pas, il faut aussi dire que le texte utilise les balises Pango, ce qui est possible avec la fonction suivante :

    Sélectionnez
    void gtk_label_set_use_markup(GtkLabel *label, gboolean setting);

    Il faut mettre le paramètre setting à TRUE, et le texte sera alors formaté en conséquence.

    Un autre moyen de spécifier l'utilisation des balises, est d'utiliser cette fonction :

    Sélectionnez
    void gtk_label_set_markup(GtkLabel *label, const gchar *str);

    Cette fonction dit à Pango qu'il faut utiliser les balises, mais elle modifie aussi le label en affichant la chaîne de caractères str (deuxième paramètre).

    IV-C-3. La balise <span>

    Cette fois-ci, nous allons étudier la balise <span> en détail. Avec celle-ci, nous allons pouvoir changer la police de caractères, la couleur du texte et bien d'autres choses.

    Cette balise s'utilise comme les précédentes si ce n'est qu'elle accepte des paramètres. Le tableau ci-dessous présente tous les paramètres et les effets sur le texte.

    Paramètre | Utilisation | Exemple
font_family | Pour définir la police à utiliser. Si la valeur donnée à ce paramètre est incorrecte, la police par défaut (Sans Serif) sera utilisée. Exemple : <span font_family=\"Courier New\">Courier New</span> | Image non disponible
face | Identique à font_family Exemple : <span face=\"Times New Roman\">Times New Roman</span> | Image non disponible
size | Pour définir la taille du texte (par défaut 10). Exemple : <span size=\"12\">Taille 12</span> | Image non disponible
style | Pour définir le style du texte. Trois valeurs possibles : normal, oblique, italic. Exemple : <span style=\"oblique\">Oblique</span> | Image non disponible
font_desc | Permet de combiner les paramètres précédents en un seul. Exemple : <span font_desc=\"Courier New italic 12\">Courier New italic 12</span> | Image non disponible
weight | Permet de définir l'épaisseur des lettres. Les valeurs peuvent être ultralight, light, normal, bold, utrabold, heavy ou encore une valeur numérique. (Vous remarquerez qu'il n'y a que peu ou pas de différence). Exemple : <span weight=\"bold\">Bold</span> | Image non disponible
variant | Pour définir si l'on veut du texte normal (normal) ou en petite majuscule (smallcaps). Exemple : <span variant=\"smallcaps\">Smallcaps</span> Afin de pourvoir l'utiliser, il faut avoir la police : nom smallcaps |
stretch | Permet d'étirer le texte. La valeur de ce paramètre peut être : ultracondensed, extracondensed, condensed, semicondensed, normal, semiexpanded, expanded, extraexpanded, ultraexpanded. Afin de pourvoir l'utiliser, il faut avoir la police : nom condensed (ou autre). |
foreground | Pour définir la couleur du texte. Il faut donner la valeur hexadécimale de la palette RVB. Exemple : <span foreground=\"#FF0000\">Couleur Texte</span> | Image non disponible
background | Pour définir la couleur du fond. Il faut donner la valeur hexadécimale de la palette RVB. Exemple : <span background=\"#FF0000\">Couleur Texte</span> | Image non disponible
underline | Pour souligner le texte. Exemple : <span underline=\"double\">Double</span> | Image non disponible
rise | Pour déplacer le texte verticalement. |
strikethrough | Pour barrer le texte. Exemple : <span strikethrough=\"true\">Striketrough = "true"</span> | Image non disponible
lang | Pour indiquer la langue du texte. |

    Tous ces paramètres peuvent être mis à l'intérieur d'une seule balise <span>.

    IV-C-4. Alignement du texte

    Maintenant, nous allons voir comment aligner notre texte, lorsque celui-ci comporte plusieurs lignes. Comme d'habitude, GTK+ nous fourni une fonction très simple d'utilisation :

    Sélectionnez
    void gtk_label_set_justify(GtkLabel *label, GtkJustification jtype);

    Le paramètre jtype correspond à l'alignement du texte et peut prendre une de ces valeurs :

    • GTK_JUSTIFY_LEFT pour aligner le texte à gauche (par défaut) ;
    • GTK_JUSTIFY_RIGHT pour aligner le texte à droite ;
    • GTK_JUSTIFY_CENTER pour centrer le texte ;
    • GTK_JUSTIFY_FILL pour justifier le texte.

    Au contraire, pour obtenir l'alignement du texte, la fonction est :

    Sélectionnez
    GtkJustification gtk_label_get_justify(GtkLabel *label);

    IV-C-5. Programme exemple

    Sélectionnez
    #include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc,char **argv)
{
    GtkWidget* pWindow;
    GtkWidget* pLabel;
    gchar* sUtf8;

    gtk_init(&argc,&argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow),"Les labels III");
    gtk_window_set_default_size(GTK_WINDOW(pWindow),320,200);

    /* Creation du label avec g_locale_to_utf8 */
    pLabel=gtk_label_new(NULL);

    /* On utilise les balises */
    sUtf8 = g_locale_to_utf8("<span face=\"Courier New\"><b>Courier New 10 Gras</b></span>\n"
        "<span font_desc=\"Times New Roman italic 12\" foreground=\"#0000FF\">Times New Roman 12 Italique</span>\n"
        "<span face=\"Sans\" size=\"16\"><u>Sans 16 Souligné</u></span>",
        -1, NULL, NULL, NULL);
    gtk_label_set_markup(GTK_LABEL(pLabel), sUtf8);
    g_free(sUtf8);

    /* On centre le texte */
    gtk_label_set_justify(GTK_LABEL(pLabel), GTK_JUSTIFY_CENTER);

     /* On ajoute le label a l'interieur de la fenetre */
     gtk_container_add(GTK_CONTAINER(pWindow),pLabel);

     /* Affichage de la fenetre et de tout ce qu'il contient */
    gtk_widget_show_all(pWindow);

    /* Connexion du signal
    /* On appelle directement la fonction de sortie de boucle */
    g_signal_connect(G_OBJECT(pWindow),"destroy",G_CALLBACK(gtk_main_quit),0);

     gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    IV-D. En savoir plus

    IV-D-1. Fonctions documentées

    Sélectionnez
    void gtk_label_set_text(GtkLabel* label, const char* str)
void gtk_label_set_label(GtkLabel* label, const gchar* str)

    Ces deux fonctions permettent de changer le texte d'un GtkLabel.

    Entrée(s) :

    • label : objet GtkLabel.
    • str : nouveau texte.
    • Sortie : rien.
    Sélectionnez
    G_CONST_RETURN gchar* gtk_label_get_text(GtkLabel* label)
G_CONST_RETURN gchar* gtk_label_get_label(GtkLabel* label)

    Permet de recevoir l'adresse de la chaîne de caractère affichée par le label.

    Entrée(s) :

    • label : objet GtkLabel.
    • Sortie : const gchar*.
    Sélectionnez
    gboolean gtk_label_get_use_markup(GtkLabel *label);

    Permet de savoir si un label utilise les balises de formatages Pango.

    Entrée(s) :

    • label : objet GtkLabel.
    • Sortie : gboolean, si oui TRUE, sinon FALSE.

    IV-D-2. Fonctions non documentées

    Sélectionnez
    void gtk_label_set_attributes(GtkLabel *label, PangoAttrList *attrs);
void gtk_label_set_markup_with_mnemonic(GtkLabel *label, const gchar *str);
void gtk_label_set_pattern(GtkLabel *label, const gchar *pattern);
guint gtk_label_parse_uline(GtkLabel *label, const gchar *string);
void gtk_label_set_line_wrap(GtkLabel *label, gboolean wrap);
void gtk_label_get_layout_offsets(GtkLabel *label, gint *x, gint *y);
guint gtk_label_get_mnemonic_keyval(GtkLabel *label);
gboolean gtk_label_get_selectable(GtkLabel *label);
GtkWidget* gtk_label_new_with_mnemonic(const char *str);
void gtk_label_select_region(GtkLabel *label, gint start_offset, gint end_offset);
void gtk_label_set_mnemonic_widget(GtkLabel *label, GtkWidget *widget);
void gtk_label_set_selectable(GtkLabel *label, gboolean setting);
void gtk_label_set_text_with_mnemonic(GtkLabel *label, const gchar *str);
PangoAttrList* gtk_label_get_attributes(GtkLabel *label);
PangoLayout* gtk_label_get_layout(GtkLabel *label);
gboolean gtk_label_get_line_wrap(GtkLabel *label);
GtkWidget* gtk_label_get_mnemonic_widget(GtkLabel *label);
gboolean gtk_label_get_selection_bounds(GtkLabel *label, gint *start, gint *end);
gboolean gtk_label_get_use_underline(GtkLabel *label);
void gtk_label_set_use_underline(GtkLabel *label, gboolean setting);


Cours GTK 2

Table des matières

  • V. Les boutons (partie 1)
    • V-A. Création et affichage d'un bouton
      • V-A-1. Créer un bouton avec du texte
      • V-A-2. Quitter l'application en cliquant sur le bouton
      • V-A-3. Affichage dans une fenêtre
      • V-A-4. Programme exemple
    • V-B. En savoir plus
      • V-B-1. Signaux
      • V-B-2. Fonctions documentées

    • V. Les boutons (partie 1)

    Nous allons cette fois nous intéresser à un élément essentiel d'une interface graphique : le bouton. En effet celui-ci permet à l'utilisateur d'effectuer une action grâce à un simple clic de souris. GTK+ nous permet de les utiliser grâce au widget GtkButton :

    Image non disponible

    V-A. Création et affichage d'un bouton

    Dans ce chapitre, notre objectif sera de créer une application contenant un bouton qui permettra de quitter l'application.

    V-A-1. Créer un bouton avec du texte

    Dans un premier temps, nous allons déclarer un pointeur vers une structure GtkWidget afin de pouvoir ensuite créer le bouton.

    GtkWidget *pQuitBtn;

    Ensuite, il faut initialiser le bouton. Pour cela, GTK+ permet l'utilisation de quatre fonctions différentes :

    GtkWidget* gtk_button_new(void);
GtkWidget* gtk_button_new_with_label(const gchar *label);
GtkWidget* gtk_button_new_with_mnemonic(const gchar *label);
GtkWidget* gtk_button_new_from_stock(const gchar *stock_id);

    La première fonction permet de créer un bouton vide. Cela permet de personnaliser complètement le bouton car GtkButton dérive de GtkContainer. On peut donc inclure n'importe quel type de widget dans le bouton (label, image…).

    La deuxième fonction s'occupe en plus d'insérer un label à l'intérieur du bouton. Le paramètre label correspond au texte à afficher. Comme pour le widget GtkLabel, si un caractère accentué est utilisé, il faudra appeler la fonction g_locale_to_utf8 pour avoir un affichage correct du texte (voir IV. Les labels pour plus de précision).

    La troisième fonction ajoute à cela une nouvelle fonctionnalité. Elle permet, en plus d'afficher un label, de faire réagir le bouton à l'aide d'un raccourci clavier. La touche servant de raccourci est spécifiée dans le paramètre label. Il suffit de mettre « _ » devant la lettre souhaitée pour que la combinaison Atl+Touche active le bouton. Par exemple pour l'application de ce chapitre, le texte à afficher sera « Quitter » et si nous voulons que la combinaison de touches Atl+Q permette de quitter l'application, le paramètre label devra être « _Quitter ».

    La quatrième fonction permet de créer un bouton avec un label, un raccourci clavier et une image. Cependant, pour faire cela, GTK+ utilise les GtkStockItem qui est une structure contenant les informations sur le label et l'image à afficher. GTK+ comporte déjà beaucoup de GtkStockItem prédéfinis (en tout cas les plus courants). Le paramètre stock_id est donc l'identifiant du GtkStockItem à utiliser. Pour notre exemple, l'identifiant est GTK_STOCK_QUIT.

    V-A-2. Quitter l'application en cliquant sur le bouton

    Pour cela, nous allons surveiller le signal « clicked » qui est émis lorsque l'utilisateur clique sur le bouton. Lorsque ce signal est reçu nous allons appeler gtk_main_quit pour fermer l'application et détruire tous les widgets. La ligne de code est donc :

    g_signal_connect(G_OBJECT(pQuitBtn), "clicked", G_CALLBACK(gtk_main_quit), NULL);

    V-A-3. Affichage dans une fenêtre

    Comme pour le chapitre précédent, on utilise la fonction gtk_container_add pour insérer le bouton dans la fenêtre et gtk_widget_show_all pour afficher le tout.

    V-A-4. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>
#define EXEMPLE_1 0
#define EXEMPLE_2 1
#define EXEMPLE_3 2
void AddBtn(GtkWidget *pWindow, gint iExemple);
int main(int argc, char **argv)
{
    GtkWidget* pWindow;
    gtk_init(&argc, &argv);
    /* Creation de la fenetre */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320 ,200);

    /* Connexion du signal "destroy" de la fenetre */
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    /* Insertion du bouton */
    AddBtn(pWindow, EXEMPLE_1);
    /* Affichage de la fenetre */
    gtk_widget_show_all(pWindow);
    /* Demarrage de la boucle evenementielle */
    gtk_main();
    return EXIT_SUCCESS;
}
/*
  void AddBtn(GtkWidget *pWindow, gint iExemple)

  Fonction en charge d'inserer le bouton dans la fenetre

  Parametre :
  - pWindow : fenetre parente
  - iExemple : mode de creation
  EXEMPLE_1 pour un bouton label
  EXEMPLE_2 pour un bouton EXEMPLE_1 + raccourci
  EXEMPLE_3 pour un bouton EXEMPLE_2 + image
*/
void AddBtn(GtkWidget *pWindow, gint iExemple)
{
    GtkWidget *pQuitBtn;
    switch(iExemple)
    {
    default:
    case EXEMPLE_1:
        /* Bouton avec un label */
        pQuitBtn = gtk_button_new_with_label("Quitter");
        gtk_window_set_title(GTK_WINDOW(pWindow), "Les boutons - Exemple 1");
        break;
    case EXEMPLE_2:
        /* Bouton avec un label et un raccourci */
        pQuitBtn = gtk_button_new_with_mnemonic("_Quitter");
        gtk_window_set_title(GTK_WINDOW(pWindow), "Les boutons - Exemple 2");
        break;
    case EXEMPLE_3:
        /* Bouton avec un label, un raccourci et une image */
        pQuitBtn = gtk_button_new_from_stock(GTK_STOCK_QUIT);
        gtk_window_set_title(GTK_WINDOW(pWindow), "Les boutons - Exemple 3");
        break;
    }
    /* Connexion du signal "clicked" du bouton */
    g_signal_connect(G_OBJECT(pQuitBtn), "clicked", G_CALLBACK(gtk_main_quit), NULL);
    /* Insertion du bouton dans la fenetre */
    gtk_container_add(GTK_CONTAINER(pWindow), pQuitBtn);
}

    Résultat :

    Image non disponible

    iExemple = EXEMPLE_1

    Image non disponible

    iExemple = EXEMPLE_2

    Image non disponible

    iExemple = EXEMPLE_3

    V-B. En savoir plus

    V-B-1. Signaux

    Prototypes fonctions callback :

    • activate

      • Ce signal est émis lorsque le bouton a le focus est que l'on appuie sur la touche « Enter ».

        void user_function(GtkButton *button, gpointer user_data);

    • enter

      • Ce signal est émis lorsque le pointeur de la souris entre dans la zone du bouton.

        void user_function(GtkButton *button, gpointer user_data);

    • leave

      • Ce signal est émis lorsque le pointeur de la souris quitte la zone du bouton.

        void user_function(GtkButton *button, gpointer user_data);

    • pressed

      • Ce signal est émis au moment où l'on appuie sur le bouton.

        void user_function(GtkButton *button, gpointer user_data);

    • released

      • Ce signal est émis au moment où l'on relâche le bouton.

        void user_function(GtkButton *button, gpointer user_data);

    V-B-2. Fonctions documentées

    void gtk_button_pressed(GtkButton *button);

    Emet le signal « pressed » pour le GtkButton concerné.

    Entrée(s) :

    • button : le bouton.

    Sortie : rien.

    void gtk_button_released(GtkButton *button);

    Emet le signal « released » pour le GtkButton concerné.

    Entrée(s) :

    • button :le bouton.

    Sortie : rien.

    void gtk_button_clicked(GtkButton *button);

    Emet le signal « clicked » pour le GtkButton concerné.

    Entrée(s) :

    • button : le bouton.

    Sortie : rien.

    void gtk_button_enter(GtkButton *button);

    Emet le signal « enter » pour le GtkButton concerné.

    Entrée(s) :

    • button : le bouton.

    Sortie : rien.

    void gtk_button_leave(GtkButton *button);

    Emet le signal « leave » pour le GtkButton concerné.

    Entrée(s) :

    • button : le bouton.

    Sortie :rien.

    void gtk_button_set_relief(GtkButton *button, GtkReliefStyle newstyle);

    Définit le style du bouton.

    Entrée(s) :

    • button : le bouton.
    • newstyle : style du bouton.

    • Les différents styles sont :

      • GTK_RELIEF_NORMAL (par défaut) ;
      • GTK_RELIEF_HALF ;
      • GTK_RELIEF_NONE.

    Sortie : rien.

    GtkReliefStyle gtk_button_get_relief(GtkButton *button);

    Récupère le style du bouton.

    Entrée(s) :

    • button : le bouton.

    Sortie : GtkReliefStyle.

    void gtk_button_set_label(GtkButton *button, const gchar *label);

    Modifie le texte d'un bouton.

    Entrée(s) :

    • button : le bouton.
    • label : le texte à afficher.

    Sortie : rien.

    G_CONST_RETURN gchar* gtk_button_get_label(GtkButton *button);

    Récupère le texte d'un bouton.

    Entrée(s) :

    • button : le bouton.

    Sortie : const gchar*.

    gboolean gtk_button_get_use_stock(GtkButton *button);

    Pour savoir si un bouton utilise les GtkStockItem.

    Entrée(s) :

    • button : le bouton.

    Sortie : gboolean, TRUE si le bouton utilise un GtkStockItem, FALSE sinon.

    void gtk_button_set_use_stock(GtkButton *button, gboolean use_stock);

    Définit si le bouton utilise un objet GtkStockItem.

    Entrée(s) :

    • button : le bouton.
    • use_stock : TRUE pour utiliser GtkStockItem, FALSE sinon.

    Sortie : rien.

    gboolean gtk_button_get_use_underline(GtkButton *button);

    Pour savoir si un bouton utilise un raccourci clavier.

    Entrée(s) :

    • button : le bouton.

    Sortie :gboolean, TRUE si le bouton utilise un raccourci, FALSE sinon.

    void gtk_button_set_use_underline(GtkButton *button, gboolean use_underline);

    Définit le bouton utilise un raccourci clavier.

    Entrée(s) :

    • button : le bouton.
    • use_underline : TRUE pour utiliser le raccourci, FALSE sinon.

    Sortie : rien.


Cours GTK 2

Table des matières

  • VI. Les box
    • VI-A. Utilisation des GtkBox
      • VI-A-1. Création
      • VI-A-2. Insertion d'un widget
      • VI-A-3. Exemple
      • VI-A-4. Programme exemple
    • VI-B. En savoir plus
      • VI-B-1. Fonctions documentées
      • VI-B-2. Fonctions non documentées

    • VI. Les box

    Vous avez sûrement dû essayer de mettre plusieurs widgets dans une fenêtre mais sans succès. Cela est dû au fait qu'un widget de type GtkContainer ne peut contenir qu'un seul widget. La solution à ce problème est l'utilisation des widgets de type GtkBox qui permettent d'inclure plusieurs widgets à l'intérieur.

    Il existe deux catégories de GtkBox :

    • Les GtkHBox qui permettent de disposer les widgets horizontalement ;
    • Les GtkVBox pour les disposer verticalement.
    Image non disponible

    VI-A. Utilisation des GtkBox

    VI-A-1. Création

    Comme toujours, la création de ces widgets est très simple. Les fonctions suivantes permettent de créer respectivement une GtkHBox et une GtkVBox :

    Sélectionnez
    GtkWidget* gtk_hbox_new(gboolean homogeneous, gint spacing);
GtkWidget* gtk_vbox_new(gboolean homogeneous, gint spacing);

    Le paramètre homogeneous définit si tous les widgets contenus dans la GtkBox utilisent un espace équivalent. C'est-à-dire que si ce paramètre est à TRUE, la zone d'affichage de la GtkBox sera divisée en x zone(s) de taille égale (x étant le nombre de widgets contenus).

    Le paramètre spacing permet de définir l'espacement entre chacun des widgets contenus.

    VI-A-2. Insertion d'un widget

    Les widgets GtkHBox et GtkVBox n'ont pas de fonctions spécifiques pour l'ajout de widget. Il faut, pour cela, utiliser les fonctions de GtkBox dont dérivent les différents types de box. Les fonctions les plus couramment utilisées sont :

    Sélectionnez
    void gtk_box_pack_start(GtkBox* box, GtkWidget* child, gboolean expand, gboolean fill, guint padding);

void gtk_box_pack_end(GtkBox* box, GtkWidget* child, gboolean expand, gboolean fill, guint padding);

    La première fonction insère les widgets de haut en bas (pour les GtkVBox) ou de gauche à droite (pour les GtkHBox). La seconde fonction fait exactement le contraire, c'est-à-dire, de bas en haut pour les GtkVBox et de droite à gauche pour les GtkHBox.

    Le paramètre box est bien entendu la GtkBox dans laquelle on veut insérer le widget child (2ème paramètre).

    Le paramètre expand n'est utile que si la GtkBox en question n'est pas défini comme homogène (homogeneous=FALSE lors de la création). Dans ce cas, tous les widgets qui auront été insérés avec la valeur expand=TRUE se partageront tout l'espace libre de la GtkBox (les widgets avec expand=FALSE n'utiliseront que l'espace qui leur est nécessaire).

    Le paramètre fill permet de définir si le widget enfant occupe toute la zone qui lui est réservée.

    Et enfin, le paramètre padding permet d'ajouter de l'espace autour du widget (en plus de celui défini par le paramètre spacing lors de la création de la GtkBox).

    Pour vous montrer les effets des différents paramètres voici un tableau avec des captures d'écran avec différentes configurations :

    Paramètres | Captures
GtkVBox : homogeneous = TRUE Bouton 1 : fill = FALSE Bouton 2 : fill = FALSE | Image non disponible
GtkVBox : homogeneous = TRUE Bouton 1 : fill = FALSE Bouton 2 : fill = TRUE | Image non disponible
GtkVBox : homogeneous = FALSE Bouton 1 : Expand = FALSE Bouton 2 : expand = FALSE | Image non disponible
GtkVBox : homogeneous = FALSE Bouton 1 : expand = FALSE Bouton 2 : expand = TRUE fill = FALSE | Image non disponible
GtkVBox : homogeneous = FALSE Bouton 1 : expand = FALSE Bouton 2 : expand = TRUE fill = TRUE | Image non disponible

    VI-A-3. Exemple

    Pour montrer les possibilités qu'offrent les GtkBox, nous allons créer une fenêtre contenant quatre boutons organisés comme cela :

    Image non disponible

    Maintenant, la question est de savoir comment organiser les différentes GtkBox pour avoir ce résultat. C'est très simple, nous avons besoin de deux GtkBox, la première verticale, la deuxième horizontale. Voici une image qui vous explique comment nous devons organiser nos deux GtkBox :

    Image non disponible

    Ici, en rouge c'est la GtkVBox qui est directement ajouté à la fenêtre principale. On va y empiler les widgets de haut en bas avec la fonction gtk_box_pack_start. On va tout d'abord placer le « Bouton 1 », ensuite pour pouvoir ajouter les autres boutons de gauche à droite, il faut ajouter la GtkHBox (en bleu) car on ne peut pas le faire avec la GtkVBox puisqu'elle est verticale. On ajoute donc ensuite « Bouton 2 » et « Bouton 3 » dans la GtkHBox, et pour finir, on ajoute « Bouton 4 » dans la GtkVBox.

    Voilà, une fois que l'on a compris comment organiser les choses, on peut tout faire.

    VI-A-4. Programme exemple

    Sélectionnez
    #include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pHBox;
    GtkWidget *pButton[4];

    gtk_init(&argc,&argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "Les GtkBox");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    /* Création de la GtkBox verticale */
    pVBox = gtk_vbox_new(TRUE, 0);
    /* Ajout de la GtkVBox dans la fenetre */
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    /* Creation des boutons */
    pButton[0] = gtk_button_new_with_label("Bouton 1");
    pButton[1] = gtk_button_new_with_label("Bouton 2");
    pButton[2] = gtk_button_new_with_label("Bouton 3");
    pButton[3] = gtk_button_new_with_label("Bouton 4");

    /* Ajout de Bouton 1 dans la GtkVBox */
    gtk_box_pack_start(GTK_BOX(pVBox), pButton[0], TRUE, FALSE, 0);

    /* Création de la box horizontale */
    pHBox = gtk_hbox_new(TRUE, 0);

    /* Ajout de la GtkHBox dans la GtkVBox */
    gtk_box_pack_start(GTK_BOX(pVBox), pHBox, TRUE, TRUE, 0);

    /* Ajout des boutons 2 et 3 dans la GtkHBox */
    gtk_box_pack_start(GTK_BOX(pHBox), pButton[1], TRUE, TRUE, 0);
    gtk_box_pack_start(GTK_BOX(pHBox), pButton[2], TRUE, FALSE, 0);

    /* Ajout du dernier bouton dans la GtkVBox */
    gtk_box_pack_start(GTK_BOX(pVBox), pButton[3], TRUE, TRUE, 0);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    VI-B. En savoir plus

    VI-B-1. Fonctions documentées

    Sélectionnez
    gboolean gtk_box_get_homogeneous(GtkBox *box);

    Utilisée pour savoir si une GtkBox est homogène.

    Entrée(s) :

    • box : la GtkBox.

    Sortie : TRUE si box est homogène, FALSE sinon.

    Sélectionnez
    void gtk_box_set_homogeneous(GtkBox *box, gboolean homogeneous);

    Utilisée pour définir si une GtkBox est homogène ou pas.

    Entrée(s) :

    • box : la GtkBox.
    • homogeneous : TRUE si l'on veut que box soit homogène, FALSE sinon.

    Sortie : rien.

    Sélectionnez
    gint gtk_box_get_spacing(GtkBox *box);

    Utilisée pour connaître l'espace entre les widgets d'une GtkBox.

    Entrée(s) :

    • box : la GtkBox.

    Sortie : gint.

    Sélectionnez
    void gtk_box_set_spacing(GtkBox *box, gint spacing);

    Utilisée pour définir l'espacement entre les widgets.

    Entrée(s) :

    • box : la GtkBox.
    • spacing : espace entre les widgets.

    Sortie : rien.

    VI-B-2. Fonctions non documentées

    Sélectionnez
    void gtk_box_pack_start_defaults(GtkBox *box, GtkWidget *widget);
void gtk_box_pack_end_defaults(GtkBox *box, GtkWidget *widget);
void gtk_box_reorder_child(GtkBox *box, GtkWidget *child, gint position);
void gtk_box_query_child_packing(GtkBox *box, GtkWidget *child, gboolean *expand, gboolean *fill, guint *padding, GtkPackType *pack_type);
void gtk_box_set_child_packing(GtkBox *box, GtkWidget *child, gboolean expand, gboolean fill, guint padding, GtkPackType pack_type);


Cours GTK 2

Table des matières

  • VII. Les tables
    • VII-A. Introduction
    • VII-B. Utilisation du widget GtkTable.
      • VII-B-1. Création.
      • VII-B-2. Insertion de widget.
      • VII-B-3. Modifier la table
      • VII-B-4. Exemple
      • VII-B-5. Programme exemple
    • VII-C. En savoir plus
      • VII-C-1. Fonctions documentées

    • VII. Les tables

    Dans la famille des containers, voilà sûrement le widget le plus intéressant. En effet il peut être parfois douloureux de placer correctement son interface avec l'utilisation de plusieurs GtkBox. Le widget GtkTable est conçu pour résoudre ce problème car il utilise une grille invisible pour attacher les widgets mais sans pour autant perdre la puissance de GTK+ avec le redimensionnement automatique.

    Image non disponible

    VII-A. Introduction

    Au moment de la création de la GtkTable, nous allons spécifier le nombre de lignes et de colonnes, puis y placer les éléments avec 3 principales caractéristiques :

    • La position de départ et de fin de l'élément par rapport aux lignes ;
    • La position de départ et fin de l'élément par rapport aux colonnes ;
    • La façon de réagir du widget (remplir la zone, agrandir, etc.).

    Reprenons l'exemple que nous avons créé dans le chapitre précédent sur les GtkBox. Nous avons créé une fenêtre contenant quatre boutons :

    Image non disponible

    Pour faire cela sans GtkTable, nous avons dû créer deux GtkBox, une première verticale, puis une deuxième horizontale dans laquelle nous avons placé les boutons 2 et 3, puis ajouter le quatrième bouton dans la première GtkBox (verticale), voila beaucoup de travail pour pas grand-chose.

    Avec le widget GtkTable, la première chose à faire sera de choisir le nombre de lignes et de colonnes sur cette grille. Dans notre cas, il s'agit d'une grille à trois lignes et deux colonnes : Image non disponible

    Ensuite il suffit de placer nos boutons sur cette grille, en spécifiant en premier les points de départ et d'arrivée suivant les colonnes puis suivant les lignes :

    • Bouton 1 : de 0 à 2 suivant les colonnes et de 0 à 1 suivant les lignes -> (0, 2, 0, 1) ;
    • Bouton 2 : de 0 à 1 suivant les colonnes et de 1 à 2 suivant les lignes -> (0, 1, 1, 2) ;
    • Bouton 3 : de 1 à 2 suivant les colonnes et de 1 à 2 suivant les lignes -> (1, 2, 1, 2) ;
    • Bouton 4 : de 0 à 2 suivant les colonnes et de 2 à 3 suivant les lignes -> (0, 2, 2, 3).

    Comme vous pouvez le constater, on ne donne pas la longueur et la largeur du widget, mais sa position finale sur la grille. Cela devient nettement plus facile et naturel de positionner les widgets sur une grille, car si ici nous ne gagnons que deux opérations par rapport à la méthode GtkBox, cette valeur s'élève très vite quand il s'agit de placer plus de 4 boutons. Après ces quelques explications, voyons comment programmer cela.

    VII-B. Utilisation du widget GtkTable.

    VII-B-1. Création.

    La fonction de création est :

    Sélectionnez
    GtkWidget* gtk_table_new(guint rows, guint columns, gboolean homogeneous);

    Les paramètres rows et columns permettent de définir respectivement le nombre de lignes et de colonnes de la grille. Le paramètre homogeneous quant à lui définit, comme pour une GtkBox, si tous les widgets contenus dans la GtkTable utilisent un espace équivalent.

    VII-B-2. Insertion de widget.

    La première fonction étudiée est :

    Sélectionnez
    void gtk_table_attach( GtkTable *table, GtkWidget *child,

    guint left_attach, guint right_attach,

    guint top_attach, guint bottom_attach,

    GtkAttachOptions xoptions, GtkAttachOptions yoptions,

    guint xpadding, guint ypadding);

    À première vue, cette fonction peut apparaître compliquée, mais elle est en réalité très simple. Le paramètre child représente le widget à attacher à la grille, les paramètres left_attach et right_attach , les positions à gauche et à droite du widget et les paramètres top_attach et bottom_attach , les positions supérieures et inférieures du widget.

    Les paramètres xoptions et yoptions permettent de spécifier respectivement la façon dont le widget réagit horizontalement et verticalement au redimensionnement de la GtkTable. Ces paramètres peuvent prendre 3 valeurs (que l'on peut associer) :

    • GTK_EXPAND : spécifie que cette section de la grille s'étirera pour remplir l'espace disponible ;
    • GTK_SHRINK : détermine ce qui se produira s'il y a un espace insuffisant pour répondre à la requête de taille du widget enfant, alors le widget se voit attribué une allocation réduite, ce qui peut entraîner un effet de « bords coupés » ;
    • GTK_FILL : spécifie que le widget enfant s'étirera pour remplir l'espace disponible, important que si GTK_EXPAND est défini.

    Les deux derniers paramètres xpadding et ypadding définissent l'espace supplémentaire à ajouter aux bords du widget (à droite et à gauche pour le premier, au-dessus et en dessous pour le second).

    Regardons maintenant les effets des paramètres xoptions et yoptions suivant différentes valeurs :

    Valeurs de xoptions et yoptions Résultat
    xoptions = GTK_EXPAND
    yoptions = GTK_EXPAND     		Image non disponible
    xoptions = GTK_EXPAND | GTK_FILL
    yoptions = GTK_EXPAND     		Image non disponible
    xoptions = GTK_EXPAND
    yoptions = GTK_EXPAND | GTK_FILL     		Image non disponible
    xoptions = GTK_EXPAND | GTK_FILL
    yoptions = GTK_EXPAND | GTK_FILL     		Image non disponible

    La deuxième fonction est :

    Sélectionnez
    void gtk_table_attach_defaults(GtkTable *table, GtkWidget *child,
    guint left_attach, guint right_attach,
    guint top_attach, guint bottom_attach );

    Ceci est la version simplifiée de la première fonction car elle définit automatiquement les paramètres xoptions et yoptions à GTK_EXPAND | GTK_FILL et les paramètres xpadding et ypadding à 0.

    VII-B-3. Modifier la table

    Il est possible de changer la taille de la grille après sa création à l'aide de cette fonction :

    Sélectionnez
    void gtk_table_resize(GtkTable *table, guint rows, guint columns);

    Le paramètre table est la GtkTable à modifier, et les paramètres rows et columns les nouveaux nombres de lignes et de colonnes.

    Ces deux fonctions permettent de changer l'espace d'une ligne ou d'une colonne spécifique :

    Sélectionnez
    gtk_table_set_row_spacing(GtkTable *table, guint row, guint spacing);
gtk_table_set_col_spacing(GtkTable *table, guint column, guint spacing);

    La première définit l'espace autour d'une ligne tandis que la deuxième fait la même chose pour une colonne.

    Celles-ci ont le même rôle que les deux précédentes fonctions, mais agissent sur l'ensemble de la GtkTable :

    Sélectionnez
    gtk_table_set_row_spacings(GtkTable *table, guint spacing);
gtk_table_set_col_spacings(GtkTable *table, guint spacing);

    Et pour connaître ces espacements nous avons quatre fonctions différentes :

    Sélectionnez
    guint gtk_table_get_row_spacing(GtkTable *table, guint row);
guint gtk_table_get_col_spacing(GtkTable *table, guint column);
guint gtk_table_get_default_row_spacing(GtkTable *table);
guint gtk_table_get_default_col_spacing(GtkTable *table);

    Les deux premières fonctions permettent de connaître l'espace entre ligne numéro row (ou la colonne numéro column ) et sa suivante. Les deux autres fonctions quant à elles, renvoient la valeur par défaut des espacements, c'est-à-dire la valeur qui sera utilisé au prochain ajout d'un widget.

    VII-B-4. Exemple

    Nous allons utiliser le même exemple que dans le chapitre sur les GtkBox, pour bien montrer la différence au niveau du code.

    VII-B-5. Programme exemple

    Sélectionnez
    #include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pTable;
    GtkWidget *pButton[4];

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    gtk_window_set_title(GTK_WINDOW(pWindow), "Les GtkTable");
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    /* Creation et insertion de la table 3 lignes 2 colonnes */
    pTable=gtk_table_new(3,2,TRUE);
    gtk_container_add(GTK_CONTAINER(pWindow), GTK_WIDGET(pTable));

    /* Creation des boutons */
    pButton[0]= gtk_button_new_with_label("Bouton 1");
    pButton[1]= gtk_button_new_with_label("Bouton 2");
    pButton[2]= gtk_button_new_with_label("Bouton 3");
    pButton[3]= gtk_button_new_with_label("Bouton 4");

    /* Insertion des boutons */
    gtk_table_attach(GTK_TABLE(pTable), pButton[0],
        0, 2, 0, 1,
        GTK_EXPAND | GTK_FILL, GTK_EXPAND,
        0, 0);
    gtk_table_attach_defaults(GTK_TABLE(pTable), pButton[1],
        0, 1, 1, 2);
    gtk_table_attach(GTK_TABLE(pTable), pButton[2],
        1, 2, 1, 2,
        GTK_EXPAND, GTK_EXPAND | GTK_FILL,
        0, 0);
    gtk_table_attach_defaults(GTK_TABLE(pTable), pButton[3],
        0, 2, 2, 3);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    VII-C. En savoir plus

    VII-C-1. Fonctions documentées

    Sélectionnez
    void gtk_table_set_homogeneous( GtkTable *table, gboolean homogeneous );

    Permet de définir si la table est homogène ou pas.

    Entrée(s) :

    • table : la GtkTable.
    • homogeneous : TRUE si l'on veut que table soit homogène, FALSE sinon.

    Sortie : rien.

    Sélectionnez
    gboolean gtk_table_get_homogeneous(GtkTable *table);

    Permet de savoir si la table est homogène.

    Entrée(s) :

    • table : la GtkTable.

    Sortie : TRUE si table est homogène, FALSE sinon.


Cours GTK 2

Table des matières

  • VIII. Les listes chaînées
    • VIII-A. Les listes chaînées simples : GSList
      • VIII-A-1. Création d'une GSList
      • VIII-A-2. Récupérer les données d'une GSList
      • VIII-A-3. Suppression d'élément d'un GSList
    • VIII-B. Les listes chaînées double : GList
      • VIII-B-1. Création d'une GList
      • VIII-B-2. Récupérer les données d'une GList
      • VIII-B-3. Suppression d'élément d'un GSList
    • VIII-C. En savoir plus
      • VIII-C-1. Fonctions documentées

    • VIII. Les listes chaînées

    Vous connaissez sûrement déjà les listes chaînées. Elles sont très pratiques, mais il faut s'occuper soit même de la gestion de la mémoire, de chaîner les éléments, à chaque fois que l'on veut ajouter ou supprimer un élément. La bibliothèque GLib propose des listes chaînées génériques que nous allons pouvoir utiliser dans nos applications Gtk+.

    VIII-A. Les listes chaînées simples : GSList

    Regardons tout d'abord comment est défini cet objet :

    Sélectionnez
    struct GSList
{
    gpointer data;
    struct GSList *next;
};

    Nous voyons donc qu'elle nous permet de créer la plus simple des liste chaînée, c'est à dire que chaque élément connaît son suivant et rien d'autre. La liste s'organise suivant ce schéma :

    Image non disponible

    Étudions comment créer une telle liste.

    VIII-A-1. Création d'une GSList

    Il faut d'abord avoir le pointeur sur le premier élément de la liste. Pour cela, rien de plus simple :

    Sélectionnez
    GSList *premier = NULL;

    Ensuite, il n'existe pas de fonction spécifique pour créer la liste, il suffit juste d'ajouter un élément à la liste. Plusieurs fonctions sont disponibles mais nous allons en étudier deux.

    Sélectionnez
    GSList* g_slist_append(GSList *list, gpointer data);
GSList* g_slist_prepend(GSList *list, gpointer data);

    La première g_slist_append ajoute un nouvel élément à la fin de la liste, alors que g_slist_prepend l'ajoute au début de la liste. La variable list est bien sûr la liste chaînée à laquelle on veut ajouter un élément comportant la donnée data. Il suffit de faire un cast pour donner le type de data. Si l'on veut que notre premier élément soit un widget, il suffira d'écrire la ligne de code suivante :

    Sélectionnez
    premier = g_slist_append(premier,(GtkWidget*) widget);

    La valeur de retour est très importante : elle correspond à l'adresse du premier élément de la liste. En effet, au départ notre élément premier ne pointe nulle part ( premier = NULL ), il faut donc toujours récupérer cette adresse (surtout dans le cas de g_slist_prepend où le premier élément change).

    VIII-A-2. Récupérer les données d'une GSList

    Là aussi, nous n'allons étudier que deux fonctions :

    Sélectionnez
    GSList* g_slist_nth(GSList *list, guint n);
gpointer g_slist_nth_data(GSList *list, guint n);

    List est bien sur la liste à laquelle l'élément recherché appartient, et n est la position de l'élément dans la liste (le premier élément est à n=0 ). Avec ces deux fonctions, il suffit juste de connaître la position d'un élément pour récupérer la donnée qu'il contient.

    La première fonction renvoie un pointeur sur une variable du type GSList. Il faudra donc utiliser l'opérateur -> pour récupérer la valeur data. La deuxième, par contre, renvoie directement la valeur de data.

    Si la valeur donnée à n ne fait pas partie de la liste, la valeur de retour sera NULL.

    Supposons qu'une variable de type GtkWidget soit stockée dans le premier élément d'une liste nommé liste, et que l'on veuille récupérer ce widget, il faudra alors coder :

    Sélectionnez
    temp_list = g_slist_nth(liste, 0);
widget = (GtkWidget*) (temp_list->data);

    Ou

    Sélectionnez
    widget = (GtkWidget*) g_slist_nth_data(liste, 0);

    VIII-A-3. Suppression d'élément d'un GSList

    Pour supprimer un élément définitivement d'une liste, la fonction à utiliser est la suivante :

    Sélectionnez
    GSList* g_slist_remove(GSList *list, gconstpointer data);

    Cette fonction cherche le premier élément de la liste contenant la donnée data et le supprime. La valeur de retour est là aussi très importante car elle correspond (toujours pareil) au nouveau premier élément de la liste. Si par hasard, plusieurs éléments contiennent la donnée data, seul le premier sera supprimé. Dans ce cas, pour tous les supprimer, il faut utiliser cette fonction (dont l'utilisation est identique à la première) :

    Sélectionnez
    GSList* g_slist_remove_all(GSList *list, gconstpointer data);

    Pour supprimer une liste entière, la fonction est :

    Sélectionnez
    void g_slist_free(GSList *list);

    Avec toutes ces fonctions, nous en savons suffisamment pour pouvoir utiliser une liste simple.

    VIII-B. Les listes chaînées double : GList

    Cette liste est légèrement différente car en plus de connaître son suivant, chaque élément connaît aussi l'élément qui le précède. Cette structure est définie ainsi :

    Sélectionnez
    struct GList
{
    gpointer data;
    struct GList *next;
    struct GList *prev;
};

    Cette nouvelle liste s'organise donc ainsi :

    Image non disponible

    L'étude sera cette fois beaucoup plus rapide car vous allez le voir, les fonctions pour les GList sont quasiment identiques à celles de GSList.

    VIII-B-1. Création d'une GList

    Là encore, il nous faut un pointeur sur le premier élément de la liste.

    Sélectionnez
    GList *premier = NULL;

    Comme pour les GList, il n'y a pas de fonction spécifique il suffit d'ajouter des éléments avec les fonctions suivantes :

    Sélectionnez
    GList* g_list_append(GList *list, gpointer data);
GList* g_list_prepend(GList *list, gpointer data);

    Les paramètres sont identiques que pour son homologue GSList et la valeur de retour est toujours un pointeur sur le premier élément de la liste.

    VIII-B-2. Récupérer les données d'une GList

    Une nouvelle fois, les fonctions ressemblent à celles des GSList :

    Sélectionnez
    GList* g_list_nth(GList *list, guint n);
gpointer g_list_nth_data(Glist *list, guint n);

    VIII-B-3. Suppression d'élément d'un GSList

    Les fonctions de suppression sont toujours semblables :

    Sélectionnez
    GList* g_list_remove(GList *list, gconstpointer data);
GList* g_list_remove_all(GList *list, gconstpointer data);
void g_list_free(GList *list);

    Bien sur pour ces deux types de listes, d'autres fonctions existent pour ajouter, déplacer, ordonner, supprimer des éléments. Ces dernières sont (comme à l'accoutumé) dans la section en savoir plus.

    Il n'y aura pas dans ce chapitre d'exemple d'utilisation, mais vous pourrez voir l'intérêt particulier de ces deux types de listes dans les chapitres suivants.

    VIII-C. En savoir plus

    VIII-C-1. Fonctions documentées

    Sélectionnez
    GSList* g_slist_insert(GSList *list, gpointer data, gint position);
GList* g_list_insert(GList *list, gpointer data, gint position);

    Ajoute un nouvel élément dans une liste à la position demandée.

    Entrée(s) :

    • list : la liste.
    • data : la valeur à ajouter.
    • position : la position à laquelle sera ajouté l'élément. Si cette valeur est invalide (négative ou trop grande) l'élément sera ajouté à la fin de la liste.

    Sortie : le pointeur sur le premier élément de la liste.

    Sélectionnez
    GSList* g_slist_insert_before(GSList *list, GSList *sibling, gpointer data);
GList* g_list_insert_before(GList *list, GList *sibling, gpointer data);

    Ajoute un nouvel élément avant un autre élément.

    Entrée(s) :

    • list : la liste.
    • sibling : élément avant lequel doit être insérée notre nouvelle valeur.
    • data : valeur à ajouter.

    Sortie : le pointeur sur le premier élément de la liste.

    Sélectionnez
    GSList* g_slist_remove_link(GSList *list, GSList *link);
GList* g_list_remove_link(GList *list, GList *link);

    Supprime un élément d'une liste. L'élément supprimer deviendra le premier d'un nouvelle liste.

    Entrée(s) :

    • list : la liste.
    • link : l'élément à supprimer de la liste.

    Sortie : le pointeur sur le premier élément de la liste.

    Sélectionnez
    GSList* g_slist_delete_link(GSList *list, GSList *link);
GList* g_list_delete_link(GList *list, GList *link);

    Supprime un élément d'un liste.

    Entrée(s) :

    • list : la liste.
    • link : l'élément à supprimer.

    Sortie :le pointeur sur le premier élément de la liste.

    Sélectionnez
    guint g_slist_length(GSList *list);
guint g_list_length(GList *list);

    Pour connaître le nombre d'élément d'un liste.

    Entrée(s) :

    • list : la liste.

    Sortie : le nombre d'éléments de la liste.

    Sélectionnez
    GSList* g_slist_copy(GSList *list);
GList* g_list_copy(GList *list);

    Crée une copie d'une liste.

    Entrée(s) :

    • list : la liste.

    Sortie : le pointeur sur le premier élément de la nouvelle liste.

    Sélectionnez
    GSList* g_slist_reverse(GSList *list);
GList* g_list_reverse(GList *list);

    Inverse les éléments d'une liste

    Entrée(s) :

    • list : la liste.

    Sortie : le pointeur sur le premier élément de la liste.

    Sélectionnez
    GSList* g_slist_concat(GSList *list1, GSList *list2);
GList* g_list_concat(GList *list1, GList *list2);

    Ajoute une liste à la fin d'une autre.

    Entrée(s) :

    • list1 : la liste de départ.
    • list2 : liste à ajouter à la suite de list1.

    Sortie : le pointeur sur le premier élément de la liste.

    Sélectionnez
    GList* g_list_first(GList *list);

    Pour récupérer le premier élément d'une liste.

    Entrée(s) :

    • list : la liste.

    Sortie : le pointeur sur le premier élément de la liste.

    Sélectionnez
    GSList* g_slist_last(GSList *list);
GList* g_list_last(GList *list);

    Pour récupérer le dernier élément d'une liste.

    Entrée(s) :

    • list : la liste.

    Sortie : le pointeur sur le dernier élément de la liste.

    Sélectionnez
    g_slist_next(list)
g_list_next(list)

    Une macro permettant d'obtenir l'élément suivant d'une liste.

    Entrée(s) :

    • list : la liste.

    Sortie : le pointeur sur l'élément suivant ou NULL si l'on est en fin de liste.

    Sélectionnez
    g_list_previous(list)

    Une macro permettant d'obtenir l'élément précédent d'une liste.

    Entrée(s) :

    • list : la liste.

    Sortie : un pointeur sur l'élément précédent ou NULL si on est en début de liste.

    Sélectionnez
    GSList* g_slist_find(GSList *list, gconstpointer data);
GList* g_list_find(GList *list, gconstpointer data);

    Récupère l'élément contenant une certaine donnée.

    Entrée(s) :

    • list : la liste.
    • data : la donnée à chercher.

    Sortie : un pointeur sur l'élément trouvé ou NULL s'il n'y en a pas.

    Sélectionnez
    gint g_slist_position(GSList *list, GSList *llink);
gint g_list_position(GList *list, GList *llink);

    Pour connaître la position d'un élément.

    Entrée(s) :

    • list : la liste.
    • llink : l'élément à chercher.

    Sortie : la position de l'élément dans la liste ou -1 s'il n'existe pas.

    Sélectionnez
    gint g_slist_index(GSList *list, gconstpointer data);
gint g_list_index(GList *list, gconstpointer data);

    Pour connaître la position d'un élément contenant une certaine donnée.

    Entrée(s) :

    • list : la liste.
    • data : la donnée à chercher.

    Sortie : la position de l'élément dans la liste ou -1 s'il n'existe pas.


Cours GTK 2

Table des matières

  • IX. Les entrées de saisie
    • IX-A. Création et utilisation d'un GtkEntry
      • IX-A-1. 1.1 Création
      • IX-A-2. 1.2 Utilisation
      • IX-A-3. 1.3 Exemple
      • IX-A-4. 1.4 Programme exemple
    • IX-B. Saisie d'un mot de passe
      • IX-B-1. La visibilité du texte
      • IX-B-2. 2.2 Le caractère affiché
      • IX-B-3. 2.3 Exemple
      • IX-B-4. 2.4 Programme exemple
    • IX-C. En savoir plu
      • IX-C-1. Signaux
      • IX-C-2. Fonction documentées
      • IX-C-3. Fonctions non documentées

    • IX. Les entrées de saisie

    Vous pouvez à présent faire interagir l'utilisateur avec des boutons, mais il peut aussi être utile de lui faire saisir des données. Nous allons utiliser pour cela le widget GtkEntry qui défini une zone de texte (une ligne) dans lequel l'utilisateur peut taper du texte ou alors dans lequel le programme peut afficher une information.

    Image non disponible

    IX-A. Création et utilisation d'un GtkEntry

    IX-A-1. 1.1 Création

    Pour créer un GtkEntry, nous avons à notre disposition deux fonctions différentes :

    GtkWidget* gtk_entry_new(void);
GtkWidget* gtk_entry_new_with_max_length(gint max);

    La première fonction crée un widget GtkEntry de base, tandis que la seconde permet en plus de définir le nombre maximum de caractère que l'utilisateur peut taper (paramètre max de type gint).

    IX-A-2. 1.2 Utilisation

    Nous allons maintenant comment utiliser les deux fonctions principales de ce widget.

    void gtk_entry_set_text(GtkEntry *entry, const gchar *text);
G_CONST_RETURN gchar* gtk_entry_get_text(GtkEntry *entry);

    La première fonction nous permet d'insérer du texte dans le GtkEntry. Le paramètre entry correspond bien sur au GtkEntry dans lequel on veut insérer le texte text. À noter, que cette fonction nécessite que entry soit de type GtkEntry* , il faudra donc utiliser la macro de conversion GTK_ENTRY() .

    La deuxième fonction permet de récupérer le texte qui a été tapé par l'utilisateur dans le GtkEntry entry. La valeur de retour est de type G_CONST_RETURN gchar* , c'est-à-dire qu'il faudra récupérer le texte dans une variable de type const gchar* . De plus, il est inutile d'allouer de la mémoire pour la variable qui va recevoir le texte, et donc de ne surtout pas libérer la mémoire car cela rendrait votre application instable.

    IX-A-3. 1.3 Exemple

    Comme exemple, nous allons créer une fenêtre comportant un GtkEntry, un GtkButton et un GtkLabel. Le but sera d'afficher le texte du GtkEntry dans le GtkLabel. Cette opération s'effectuera lorsque l'utilisateur appuie sur la touche ENTREE à la fin de sa saisie (interception du signal « activate ») ou lorsqu'il cliquera sur le GtkButton (interception du signal « clicked »).

    Ensuite, les fonctions callback récupèreront le texte du GtkEntry pour l'afficher dans le GtkLabel. Si cette opération ne pose aucun problème pour le signal « activate » du GtkEntry, un problème existe lorsque l'on clique sur le GtkButton.

    En effet, pour faire son travail correctement, la fonction callback doit connaître le GtkEntry et le GtkLabel. Dans le cas du signal « activate », le GtkEntry est envoyé en paramètre principal de la fonction callback (widget émetteur du signal) et l'on rajoute le GtkLabel en paramètre supplémentaire. Par contre, pour le signal « clicked », le widget émetteur est le GtkButton et l'on ne pourra passer qu'un seul widget en donnée supplémentaire.

    La solution consiste à utiliser les listes du chapitre précédent ( VII. Les listes chaînées Les listes chaînées ) ainsi qu'une fonction du widget GtkContainer :

    GList* gtk_container_get_children(GtkContainer *container);

    Cette fonction crée une liste doublement chaînée contenant tous les widgets qui ont été insérés dans le widget container. Dans notre cas, il nous suffira de passer la GtkBox contenant tous les widgets à la fonction callback pour avoir accès à tout ce dont nous avons besoin.

    IX-A-4. 1.4 Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

void on_activate_entry(GtkWidget *pEntry, gpointer data);
void on_copier_button(GtkWidget *pButton, gpointer data);

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pEntry;
    GtkWidget *pButton;
    GtkWidget *pLabel;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "Le widget GtkEntry");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    /* Creation du GtkEntry */
    pEntry = gtk_entry_new();
    /* Insertion du GtkEntry dans la GtkVBox */
    gtk_box_pack_start(GTK_BOX(pVBox), pEntry, TRUE, FALSE, 0);

    pButton = gtk_button_new_with_label("Copier");
    gtk_box_pack_start(GTK_BOX(pVBox), pButton, TRUE, FALSE, 0);

    pLabel = gtk_label_new(NULL);
    gtk_box_pack_start(GTK_BOX(pVBox), pLabel, TRUE, FALSE, 0);

    /* Connexion du signal "activate" du GtkEntry */
    g_signal_connect(G_OBJECT(pEntry), "activate", G_CALLBACK(on_activate_entry), (GtkWidget*) pLabel);

    /* Connexion du signal "clicked" du GtkButton */
    /* La donnee supplementaire est la GtkVBox pVBox */
    g_signal_connect(G_OBJECT(pButton), "clicked", G_CALLBACK(on_copier_button), (GtkWidget*) pVBox);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

/* Fonction callback execute lors du signal "activate" */
void on_activate_entry(GtkWidget *pEntry, gpointer data)
{
    const gchar *sText;

    /* Recuperation du texte contenu dans le GtkEntry */
    sText = gtk_entry_get_text(GTK_ENTRY(pEntry));

    /* Modification du texte contenu dans le GtkLabel */
    gtk_label_set_text(GTK_LABEL((GtkWidget*)data), sText);
}

/* Fonction callback executee lors du signal "clicked" */
void on_copier_button(GtkWidget *pButton, gpointer data)
{
    GtkWidget *pTempEntry;
    GtkWidget *pTempLabel;
    GList *pList;
    const gchar *sText;

    /* Recuperation de la liste des elements que contient la GtkVBox */
    pList = gtk_container_get_children(GTK_CONTAINER((GtkWidget*)data));

    /* Le premier element est le GtkEntry */
    pTempEntry = GTK_WIDGET(pList->data);

    /* Passage a l element suivant : le GtkButton */
    pList = g_list_next(pList);

    /* Passage a l element suivant : le GtkLabel */
    pList = g_list_next(pList);

    /* Cet element est le GtkLabel */
    pTempLabel = GTK_WIDGET(pList->data);

    /* Recuperation du texte contenu dans le GtkEntry */
    sText = gtk_entry_get_text(GTK_ENTRY(pTempEntry));

    /* Modification du texte contenu dans le GtkLabel */
    gtk_label_set_text(GTK_LABEL(pTempLabel), sText);

    /* Liberation de la memoire utilisee par la liste */
    g_list_free(pList);
}

    Résultat :

    Image non disponible
    Image non disponible

    IX-B. Saisie d'un mot de passe

    IX-B-1. La visibilité du texte

    Généralement, lorsque nous tapons un mot de passe, nous souhaitons que celui-ci reste secret. Le widget GtkEntry permet cela grâce à cette fonction :

    void gtk_entry_set_visibility(GtkEntry *entry, gboolean visible);

    Il suffit donc de mettre le paramètre visible à FALSE pour cacher le texte qui sera entré.

    A l'inverse, pour savoir si le texte entré sera visible ou pas, il faut utiliser cette fonction :

    gboolean gtk_entry_get_visibility(GtkEntry *entry);

    La valeur de retour sera, bien sûr, égale à TRUE si le texte est visible et à FALSE dans le cas contraire.

    IX-B-2. 2.2 Le caractère affiché

    Par défaut, lorsque l'on ajoute du texte à un GtkEntry qui a son paramètre visible à FALSE, GTK+ remplacera toutes les lettres par des '*'. Pour modifier celui-ci, il faut utiliser cette fonction :

    void gtk_entry_set_invisible_char(GtkEntry *entry, gunichar ch);

    Le paramètre ch correspond au caractère de remplacement que nous souhaitons. Celui-ci est de type gunichar qui correspond à l'encodage UCS-4. Bien que l'affichage de GTK+ se fasse avec l'encodage UTF-8, cela ne pose aucun problème car cette fois-ci, la conversion est faite automatiquement.

    Et pour terminer, la fonction permettant de connaître le caractère de remplacement est :

    gunichar gtk_entry_get_invisble_char(GtkEntry *entry);

    IX-B-3. 2.3 Exemple

    Cette fois, nous allons reprendre l'exemple précédent en activant le mode « mot de passe » et en limitant la saisie à huit caractères. Nous modifierons aussi le caractère de remplacement '*' par '$'.

    IX-B-4. 2.4 Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

void on_activate_entry(GtkWidget *pEntry, gpointer data);
void on_copier_button(GtkWidget *pButton, gpointer data);

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pEntry;
    GtkWidget *pButton;
    GtkWidget *pLabel;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "Le widget GtkEntry");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    /* Creation du GtkEntry avec 8 caracteres maximum */
    pEntry = gtk_entry_new_with_max_length(8);
    /* Mode mot de passe */
    gtk_entry_set_visibility(GTK_ENTRY(pEntry), FALSE);
    /* Modification du caractere affiche */
    gtk_entry_set_invisible_char(GTK_ENTRY(pEntry), '$');
    /* Insertion du GtkEntry dans la GtkVBox */
    gtk_box_pack_start(GTK_BOX(pVBox), pEntry, TRUE, FALSE, 0);

    pButton = gtk_button_new_with_label("Copier");
    gtk_box_pack_start(GTK_BOX(pVBox), pButton, TRUE, FALSE, 0);

    pLabel = gtk_label_new(NULL);
    gtk_box_pack_start(GTK_BOX(pVBox), pLabel, TRUE, FALSE, 0);

    /* Connexion du signal "activate" du GtkEntry */
    g_signal_connect(G_OBJECT(pEntry), "activate", G_CALLBACK(on_activate_entry), (GtkWidget*) pLabel);

    /* Connexion du signal "clicked" du GtkButton */
    /* La donnee supplementaire est la GtkVBox pVBox */
    g_signal_connect(G_OBJECT(pButton), "clicked", G_CALLBACK(on_copier_button), (GtkWidget*) pVBox);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

/* Fonction callback execute lors du signal "activate" */
void on_activate_entry(GtkWidget *pEntry, gpointer data)
{
    const gchar *sText;

    /* Recuperation du texte contenu dans le GtkEntry */
    sText = gtk_entry_get_text(GTK_ENTRY(pEntry));

    /* Modification du texte contenu dans le GtkLabel */
    gtk_label_set_text(GTK_LABEL((GtkWidget*)data), sText);
}

/* Fonction callback executee lors du signal "clicked" */
void on_copier_button(GtkWidget *pButton, gpointer data)
{
    GtkWidget *pTempEntry;
    GtkWidget *pTempLabel;
    GList *pList;
    const gchar *sText;

    /* Recuperation de la liste des elements que contient la GtkVBox */
    pList = gtk_container_get_children(GTK_CONTAINER((GtkWidget*)data));

    /* Le premier element est le GtkEntry */
    pTempEntry = GTK_WIDGET(pList->data);

    /* Passage a l element suivant : le GtkButton */
    pList = g_list_next(pList);

    /* Passage a l element suivant : le GtkLabel */
    pList = g_list_next(pList);

    /* Cet element est le GtkLabel */
    pTempLabel = GTK_WIDGET(pList->data);

    /* Recuperation du texte contenu dans le GtkEntry */
    sText = gtk_entry_get_text(GTK_ENTRY(pTempEntry));

    /* Modification du texte contenu dans le GtkLabel */
    gtk_label_set_text(GTK_LABEL(pTempLabel), sText);

    /* Liberation de la memoire utilisee par la liste */
    g_list_free(pList);
}

    Résultat :

    Image non disponible

    IX-C. En savoir plu

    IX-C-1. Signaux

    Prototypes fonctions callback :

    • activate

      • void user_function(GtkEntry *entry, gpointer user_data);

    • copy-clipboard

      • void user_function(GtkEntry *entry, gpointer user_data);

    • cut-clipboard

      • void user_function(GtkEntry *entry, gpointer user_data);

    • delete-from-cursor

      • void user_function(GtkEntry *entry, GtkDeleteType arg1, gint arg2, gpointer user_data);

    • insert-at-cursor

      • void user_function(GtkEntry *entry, gchar *arg1, gpointer user_data);

    • move-cursor

      • void user_function(GtkEntry *entry, GtkMovementStep arg1, gint arg2, gboolean arg3, gpointer user_data);

    • paste-clipboard

      • void user_function(GtkEntry *entry, gpointer user_data);

    • populate-popup

      • void user_function(GtkEntry *entry, GtkMenu *arg1, gpointer user_data);

    • toggle-overwrite

      • void user_function (GtkEntry *entry, gpointer user_data);

    IX-C-2. Fonction documentées

    void gtk_entry_set_max_length(GtkEntry *entry, gint max);

    Définit le nombre maximum de caractère que l'utilisateur peut saisir.

    Entrée(s) :

    • entry : le GtkEntry.
    • max : le nombre de caractère.

    Sortie : rien.

    gint gtk_entry_get_max_length(GtkEntry *entry);

    Récupère le nombre maximum de caractère que l'utilisateur peut saisir.

    Entrée(s) :

    • entry : le GtkEntry.

    Sortie : le nombre maximum de caractères.

    IX-C-3. Fonctions non documentées

    gboolean gtk_entry_get_activates_default(GtkEntry *entry);
void gtk_entry_set_activates_default(GtkEntry *entry, gboolean setting);
gboolean gtk_entry_get_has_frame(GtkEntry *entry);
void gtk_entry_set_has_frame(GtkEntry *entry, gboolean setting);
gint gtk_entry_get_width_chars(GtkEntry *entry);
void gtk_entry_set_width_chars(GtkEntry *entry, gint n_chars);
PangoLayout* gtk_entry_get_layout(GtkEntry *entry);
void gtk_entry_get_layout_offsets(GtkEntry *entry, gint *x, gint *y);


Cours GTK 2

Table des matières

  • X. Les décorations
    • X-A. Le cadre
      • X-A-1. Création
      • X-A-2. Modification du texte
      • X-A-3. Remplacer le texte par un widget
      • X-A-4. Position du texte
      • X-A-5. Style du cadre
    • X-B. Les lignes
      • X-B-1. Création
    • X-C. Exemple
      • X-C-1. Description
      • X-C-2. Programme exemple

    • X. Les décorations

    Maintenant, nous connaissons suffisamment de widgets pour créer des fenêtres complexes. Afin d'améliorer l'esthétique de ces fenêtres nous allons voir comment ajouter des décorations (ou séparation) entre les différentes parties de la fenêtre. Il existe deux types de décoration :

    • le cadre GtkFrame qui entoure toute une zone de la fenêtre et qui possède un texte permettant de définir la zone ;
    • la ligne GtkSeparator qui divise en deux parties différentes, le widget GtkHSeparator pour les lignes horizontales et le widget GtkVSeparator pour les lignes verticales.
    Image non disponible

    X-A. Le cadre

    X-A-1. Création

    Le widget GtkFrame étant très simple, il n'y a qu'une seule fonction de création :

    GtkWidget* gtk_frame_new(const gchar *label);

    Le paramètre label est tout simplement le texte qui sera affiché en haut à gauche du cadre (position par défaut).

    X-A-2. Modification du texte

    Il peut arriver que dans votre application, le texte du cadre nécessite une modification. Bien entendu, le widget GtkFrame est fourni avec toutes les fonctions nécessaires.

    void gtk_frame_set_label(GtkFrame *frame, const gchar *label);

    Le paramètre frame est le widget GtkFrame dont nous voulons modifier le texte, et label est le nouveau texte à inscrire. Cette fois encore, il faut utiliser une macro de conversion pour le premier paramètre qui est cette fois GTK_FRAME() .

    Pour récupérer le texte du GtkFrame, la fonction est :

    G_CONST_RETURN gchar* gtk_frame_get_label(GtkFrame *frame);

    X-A-3. Remplacer le texte par un widget

    Les cadres offrent aussi la possibilité de remplacer le texte par un widget quelconque (GtkImage, GtkStockItem…) grâce à cette fonction :

    gtk_frame_set_label_widget(GtkFrame *frame, GtkWidget *label_widget);

    Et comme toujours, la fonction permettant de connaître le widget affiché:

    GtkWidget* gtk_frame_get_label_widget(GtkFrame *frame);

    X-A-4. Position du texte

    Par défaut, la position du texte est en haut à gauche du cadre, centré en hauteur par rapport à la ligne supérieure. Cela aussi peut être modifié avec cette fonction :

    void gtk_frame_set_label_align(GtkFrame *frame, gfloat xalign, gfloat yalign);

    Les valeurs xalign et yalign doivent être comprises entre 0.0 et 1.0.

    Le paramètre xalign définit la position horizontale du texte. Une valeur de 0.0 positionne le texte à gauche du cadre, tandis qu'une valeur de 1.0 le positionne à droite. Evidemment, une valeur de 0.5 centrera le texte.

    Quant à yalign , il permet de définir la position verticale du texte par rapport à la ligne supérieur du cadre. Une valeur de 0.0 mettra le nommeur en dessous de la ligne et une valeur de 1.0 le mettra au-dessus de la ligne.

    On peut, bien sûr, connaître les valeurs de positionnement avec les fonctions suivantes :

    void gtk_frame_get_label_align(GtkFrame *frame, gfloat *xalign, gfloat *yalign);

    X-A-5. Style du cadre

    Le style du cadre correspond plus à la configuration visuelle des lignes du cadre et plus précisément encore l'ombre des lignes. Cette modification de fait avec cette fonction :

    void gtk_frame_set_shadow_type(GtkFrame *frame, GtkShadowType type);

    Le paramètre type peut prendre cinq valeurs différentes dont voici la liste avec leurs illustrations :

    Valeur de type | Illustration de la ligne supérieure
GTK_SHADOW_NONE | En fait, on ne voit pas la ligne.
GTK_SHADOW_IN | Image non disponible
GTK_SHADOW_OUT | Image non disponible
GTK_SHADOW_ETCHED_IN (par défaut) | Image non disponible
GTK_SHADOW_ETCHED_OUT | Image non disponible

    Et sans surprise, voici la fonction qui permet de connaître le type des lignes :

    GtkShadowType gtk_frame_get_shadow_type(GtkFrame *frame);

    X-B. Les lignes

    Alors cette fois, cela va être très rapide et très simple.

    X-B-1. Création

    GtkWidget* gtk_hseparator_new(void);
GtkWidget* gtk_vseparator_new(void);

    La première fonction crée une ligne horizontale alors que la deuxième crée une ligne verticale.

    Il n'y a rien de plus à propos de ce widget.

    X-C. Exemple

    X-C-1. Description

    Afin de vous montrer l'avantage visuel de l'utilisation des GtkFrame et GtkSeparator, nous allons créer une fenêtre qui demande à l'utilisateur de saisir des informations le concernant (nom, prénom, adresse…).

    Voila à quoi ressemble la fenêtre sans l'utilisation des décorations :

    Image non disponible

    Attention, ce programme ne fait rien du tout, c'est simplement pour montrer la différence entre avec et sans les décorations.

    X-C-2. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pFrame;
    GtkWidget *pVBoxFrame;
    GtkWidget *pSeparator;
    GtkWidget *pEntry;
    GtkWidget *pLabel;
    gchar *sUtf8;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    /* On ajoute un espace de 5 sur les bords de la fenetre */
    gtk_container_set_border_width(GTK_CONTAINER(pWindow), 5);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkEntry et GtkSeparator");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    /* Creation du premier GtkFrame */
    pFrame = gtk_frame_new("Etat civil");
    gtk_box_pack_start(GTK_BOX(pVBox), pFrame, TRUE, FALSE, 0);

    /* Creation et insertion d une boite pour le premier GtkFrame */
    pVBoxFrame = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pFrame), pVBoxFrame);

    /* Creation et insertion des elements contenus dans le premier GtkFrame */
    pLabel = gtk_label_new("Nom :");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    sUtf8 = g_locale_to_utf8("Prénom :", -1, NULL, NULL, NULL);
    pLabel = gtk_label_new(sUtf8);
    g_free(sUtf8);
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    /* Creation d un GtkHSeparator */
    pSeparator = gtk_hseparator_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pSeparator, TRUE, FALSE, 0);

    pLabel = gtk_label_new("Date de naissance :");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    /* Creation du deuxieme GtkFrame */
    pFrame = gtk_frame_new("Domicile");
    gtk_box_pack_start(GTK_BOX(pVBox), pFrame, TRUE, FALSE, 0);

    /* Creation et insertion d une boite pour le deuxieme GtkFrame */
    pVBoxFrame = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pFrame), pVBoxFrame);

    /* Creation et insertion des elements contenus dans le deuxieme GtkFrame */
    pLabel = gtk_label_new("Adresse :");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    pLabel = gtk_label_new("Adresse :");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    pLabel = gtk_label_new("Code postal :");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    pLabel = gtk_label_new("Ville :");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    /* Creation du troisieme GtkFrame */
    sUtf8 = g_locale_to_utf8("Téléphones", -1, NULL, NULL, NULL);
    pFrame = gtk_frame_new(sUtf8);
    g_free(sUtf8);
    gtk_box_pack_start(GTK_BOX(pVBox), pFrame, TRUE, FALSE, 0);

    /* Creation et insertion d une boite pour le troisieme GtkFrame */
    pVBoxFrame = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pFrame), pVBoxFrame);

    /* Creation et insertion des elements contenus dans le troisieme GtkFrame */
    pLabel = gtk_label_new("Domicile");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    pLabel = gtk_label_new("Professionnel");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    pLabel = gtk_label_new("Portable");
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pLabel, TRUE, FALSE, 0);
    pEntry = gtk_entry_new();
    gtk_box_pack_start(GTK_BOX(pVBoxFrame), pEntry, TRUE, FALSE, 0);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible


Cours GTK 2

Table des matières

  • XI. Les images
    • XI-A. Utilisation du widget GtkImage
      • XI-A-1. Création
      • XI-A-2. Modification de l'image
      • XI-A-3. Exemple
      • XI-A-4. Programme exemple
    • XI-B. En savoir plus
      • XI-B-1. Fonction non documentées.

    • XI. Les images

    Nous allons cette fois, essayer de rendre nos fenêtres un peu moins austères. Pour cela, nous allons utiliser des images grâce au widget GtkImage. Nous allons voir comment afficher une image à partir d'un fichier ou d'un GtkStockItem.

    Image non disponible

    XI-A. Utilisation du widget GtkImage

    XI-A-1. Création

    Pour ce widget, il y a toute une panoplie de fonction de création. Nous n'allons en étudier que quelques-unes, car certaines font appel à des notions par encore vues. Voici donc les fonctions étudiées :

    GtkWidget* gtk_image_new (void);
GtkWidget* gtk_image_new_from_file(const gchar *filename);
GtkWidget* gtk_image_new_from_stock(const gchar *stock_id, GtkIconSize size);

    La première crée une image mais complètement vide.

    La deuxième crée l'image à partir du fichier filename. Gtk+ est capable d'utiliser les images qui sont au format PNG, JPEG, TIFF. Le chemin du fichier filename peut être relatif ou absolu. Si le chemin spécifié est incorrect ou que le format de l'image est invalide, l'image de retour sera celle-ci : Image non disponible

    La troisième fonction, récupère l'image qui est associée à un objet GtkStockItem afin de l'afficher. Le paramètre size peut prendre sept valeurs différentes pour définir la taille de l'image à afficher :

    Valeur | Aperçu
GTK_ICON_SIZE_INVALID | Ne peut être passé en paramètre. Ceci est uniquement une valeur de retour.
GTK_ICON_SIZE_MENU | Image non disponible
GTK_ICON_SIZE_SMALL_TOOLBAR | Image non disponible
GTK_ICON_SIZE_LARGE_TOOLBAR | Image non disponible
GTK_ICON_SIZE_BUTTON | Image non disponible
GTK_ICON_SIZE_DND | Image non disponible
GTK_ICON_SIZE_DIALOG | Image non disponible

    XI-A-2. Modification de l'image

    Cette étape intervient lorsque vous avez créé une image vide ou lorsque vous voulez changer d'image. Les deux fonctions étudiées ici sont :

    void gtk_image_set_from_file(GtkImage *image, const gchar *filename);
void gtk_image_set_from_stock(GtkImage *image, const gchar *stock_id, GtkIconSize size);

    Les paramètres sont les mêmes que lors de la création d'un widget GtkImage, sauf qu'il faut préciser à quel widget il faut appliquer l'image.

    XI-A-3. Exemple

    Notre exemple comprendra deux composants. Tout d'abord une zone ou est affichée l'image, puis un bouton contenant l'image du GtkStockItem GTK_STOCK_QUIT, pour quitter l'application.

    XI-A-4. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pImage;
    GtkWidget *pQuitImage;
    GtkWidget *pQuitBtn;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkImage");
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    /* Chargement d'une image a partir d'un fichier */
    pImage = gtk_image_new_from_file("./gtk.png");
    gtk_box_pack_start(GTK_BOX(pVBox), pImage, FALSE, FALSE, 5);

    pQuitBtn = gtk_button_new();
    gtk_box_pack_start(GTK_BOX(pVBox), pQuitBtn, TRUE, FALSE, 5);
    g_signal_connect(G_OBJECT(pQuitBtn), "clicked", G_CALLBACK(gtk_main_quit), NULL);

    /* Chargement d'une image a partir d'un GtkStockItem */
    pQuitImage = gtk_image_new_from_stock(GTK_STOCK_QUIT, GTK_ICON_SIZE_LARGE_TOOLBAR);
    gtk_container_add(GTK_CONTAINER(pQuitBtn), pQuitImage);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    XI-B. En savoir plus

    XI-B-1. Fonction non documentées.

    void gtk_image_get_icon_set(GtkImage *image, GtkIconSet **icon_set, GtkIconSize *size);
void gtk_image_get_image(GtkImage *image, GdkImage **gdk_image, GdkBitmap **mask);
GdkPixbuf* gtk_image_get_pixbuf(GtkImage *image);
void gtk_image_get_pixmap(GtkImage *image, GdkPixmap **pixmap, GdkBitmap **mask);
void gtk_image_get_stock(GtkImage *image, gchar **stock_id, GtkIconSize *size);
GdkPixbufAnimation* gtk_image_get_animation(GtkImage *image);
GtkImageType gtk_image_get_storage_type(GtkImage *image);
GtkWidget* gtk_image_new_from_icon_set(GtkIconSet *icon_set, GtkIconSize size);
GtkWidget* gtk_image_new_from_image(GdkImage *image, GdkBitmap *mask);
GtkWidget* gtk_image_new_from_pixbuf(GdkPixbuf *pixbuf);
GtkWidget* gtk_image_new_from_pixmap(GdkPixmap *pixmap, GdkBitmap *mask);
GtkWidget* gtk_image_new_from_animation(GdkPixbufAnimation *animation);
void gtk_image_set_from_icon_set(GtkImage *image, GtkIconSet *icon_set, GtkIconSize size);
void gtk_image_set_from_image(GtkImage *image, GdkImage *gdk_image, GdkBitmap *mask);
void gtk_image_set_from_pixbuf(GtkImage *image, GdkPixbuf *pixbuf);
void gtk_image_set_from_pixmap(GtkImage *image, GdkPixmap *pixmap, GdkBitmap *mask);
void gtk_image_set_from_animation(GtkImage *image, GdkPixbufAnimation *animation);
void gtk_image_set(GtkImage *image, GdkImage *val, GdkBitmap *mask);
void gtk_image_get(GtkImage *image, GdkImage **val, GdkBitmap **mask);


Cours GTK 2

Table des matières

  • XII. Les boîtes de dialogue
    • XII-A. Utilisation d'une boîte de saisie
      • XII-A-1. Constitution d'une boîte de dialogue
      • XII-A-2. Création
      • XII-A-3. Affichage
      • XII-A-4. Exemple
      • XII-A-5. Programme exemple
    • XII-B. Les boîtes de messages
      • XII-B-1. Création
      • XII-B-2. Exemple
      • XII-B-3. Programme exemple
    • XII-C. En savoir plus
      • XII-C-1. Fonctions documentées
      • XII-C-2. Fonctions non documentées

    • XII. Les boîtes de dialogue

    Nous allons dans ce chapitre étudier un style de fenêtre particulier : les boîtes de dialogue. Elles permettent de demander à l'utilisateur des informations ou alors d'afficher des messages. Ces boîtes de dialogue ressemblant fortement à des fenêtres classiques, il est normal que le widget GtkDialog dérive directement de GtkWindow.

    XII-A. Utilisation d'une boîte de saisie

    La saisie d'information via le widget GtkEntry ne s'effectue presque jamais dans la fenêtre principale mais dans une boîte de dialogue. Nous allons donc voir comment créer une telle boîte.

    XII-A-1. Constitution d'une boîte de dialogue

    Image non disponible

    Comme nous pouvons le voir sur l'image ci-dessus, une boîte de dialogue est constituée de trois éléments :

    • Une GtkVBox globale (en rouge) qui contiendra tous les widgets affichés ;
    • Une GtkHSeparator (en vert) qui sert de séparation entre la zone de saisie et la zone des boutons ;
    • Une GtkHBox (en bleu) qui contiendra les boutons de réponse.

    On peut donc ainsi distinguer deux zones différentes :

    • La zone de travail au-dessus de la GtkHSeparator ;
    • La zone de réponse au-dessous de la GtkHSeparator.

    XII-A-2. Création

    La fonction de création est un peu plus complexe que d'habitude :

    Sélectionnez
    GtkWidget* gtk_dialog_new_with_buttons(const gchar *title, GtkWindow *parent, GtkDialogFlags flags, const gchar *first_button_text, ...);

    Le premier paramètre title n'est autre que le titre de la boîte de dialogue.

    Le deuxième paramètre parent sert à désigner la fenêtre parente. Cette valeur peut être égale à NULL.

    Le troisième paramètre flags permet de définir certains paramètres de la boîte de dialogue. Ce paramètre peut prendre trois valeurs différentes :

    • GTK_DIALOG_MODAL : créer une boîte de dialogue modale, c'est-à-dire que tant que l'utilisateur n'a pas cliqué sur un des boutons de réponse, les autres fenêtres seront figées ;
    • GTK_DIALOG_DESTROY_WITH_PARENT : la boîte de dialogue est détruite si la fenêtre parente est détruite ;
    • GTK_DIALOG_NO_SEPARATOR : la ligne de séparation entre la zone de travail et la zone de réponse n'est pas affichée.

    Enfin, le dernier paramètre est plutôt une liste de paramètres permettant de définir les boutons de la boîte de dialogue ainsi que les réponses qui leurs sont associées. Pour chaque bouton que nous voulons ajouter, il faut définir le texte du bouton et le type de réponse qui sera envoyé lorsque nous cliquerons sur le bouton.

    Le texte du bouton peut, comme pour tous les boutons, être un texte normal, un texte avec raccourci (Rappel : c'est de la forme « _Quitter ») ou même un GtkStockItem.

    La valeur de la réponse, peut être un élément de type GtkResponseType ou bien une valeur entière positive que nous pouvons définir nous-même. Le plus simple étant bien sûr d'utiliser les valeurs classiques définis par le type GtkResponseType dont voici la liste :

    • GTK_RESPONSE_NONE ;
    • GTK_RESPONSE_REJECT ;
    • GTK_RESPONSE_ACCEPT ;
    • GTK_RESPONSE_DELETE_EVENT ;
    • GTK_RESPONSE_OK ;
    • GTK_RESPONSE_CANCEL ;
    • GTK_RESPONSE_CLOSE ;
    • GTK_RESPONSE_YES ;
    • GTK_RESPONSE_NO ;
    • GTK_RESPONSE_APPLY ;
    • GTK_RESPONSE_HELP.

    Une fois que tous les boutons ont été définis, il faut le dire à notre fonction de création. Pour cela, il suffit de terminer la liste des paramètres par NULL.

    La majeure partie des éléments de la boîte de dialogue sont maintenant créés. Il reste cependant à ajouter les éléments de la zone de travail pour que la boîte soit complète. Une fois que tous les éléments à ajouter dans la zone de travail sont créés, il suffit de les ajouter dans la boîte de dialogue avec la fonction gtk_box_pack_start.

    La question est maintenant de savoir comment passer la GtkVBox en paramètre à la fonction gtk_box_pack_start. Nous allons tout simplement utiliser l'opérateur -> pour l'élément GtkDialog de cette manière :

    Sélectionnez
    GTK_DIALOG(nom_de_la_boite)->vbox;

    La boîte de dialogue est maintenant complète.

    XII-A-3. Affichage

    L'affichage de la boîte de dialogue comporte deux étapes. Tout d'abord, il faut demander d'afficher tout le contenu de la GtkVBox qui est inclu dans la boîte de dialogue avec la fonction gtk_widget_show_all.

    Ensuite il faut afficher la boîte de dialogue en elle-même et attendre la réponse de l'utilisateur avec cette fonction :

    Sélectionnez
    gint gtk_dialog_run(GtkDialog *dialog);

    Le paramètre de cette fonction étant de type GtkDialog il faut utiliser la macro de conversion GTK_DIALOG().

    Cette fonction affiche l'intégralité de la boîte de dialogue, mais rentre aussi dans une boucle récursive qui ne s'arrêtera que lorsque l'utilisateur cliquera sur un des boutons. La valeur de retour correspond à la valeur associée au bouton cliqué. Si, par hasard, l'utilisateur quitte la boîte de dialogue en cliquant sur la croix de celle-ci, gtk_dialog_run renvoie GTK_RESPONSE_NONE.

    Une fois la valeur de retour connue, il ne reste plus qu'à agir en conséquence.

    XII-A-4. Exemple

    L'exemple de ce chapitre est constitué d'une fenêtre principale dans laquelle nous allons ajouter un GtkButton et un GtkLabel. Lorsque l'utilisateur clique sur le GtkButton, une boîte de dialogue apparaîtra et demandera à l'utilisateur de saisir son nom.

    Cette boîte de dialogue sera constituée d'une GtkEntry, d'un GtkButton « OK » et d'un GtkButton « Annuler ». Si l'utilisateur clique sur « OK » le contenu de la GtkEntry sera copié dans le GtkLabel de la fenêtre principale tandis que s'il clique sur « Annuler » on affichera « Vous n'avez rien saisi. » dans le GtkLabel.

    XII-A-5. Programme exemple

    Sélectionnez
    #include <stdlib.h>
#include <gtk/gtk.h>

static GtkWidget *pLabel;
static GtkWidget *pWindow;

void lancer_boite(void);

int main(int argc, char **argv)
{
    GtkWidget *pVBox;
    GtkWidget *pButton;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkDialog");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    pButton = gtk_button_new_with_label("Cliquez ici pour saisir votre nom");
    gtk_box_pack_start(GTK_BOX(pVBox), pButton, FALSE, TRUE, 0);

    pLabel = gtk_label_new(NULL);
    gtk_box_pack_start(GTK_BOX(pVBox), pLabel, FALSE, FALSE, 0);

    /* Connexion du signal "clicked" pour ouvrir la boite de dialogue */
    g_signal_connect(G_OBJECT(pButton), "clicked", G_CALLBACK(lancer_boite), NULL);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

void lancer_boite(void)
{
    GtkWidget* pBoite;
    GtkWidget* pEntry;
    const gchar* sNom;

    /* Création de la boite de dialogue */
    /* 1 bouton Valider */
    /* 1 bouton Annuler */
    pBoite = gtk_dialog_new_with_buttons("Saisie du nom",
        GTK_WINDOW(pWindow),
        GTK_DIALOG_MODAL,
        GTK_STOCK_OK,GTK_RESPONSE_OK,
        GTK_STOCK_CANCEL,GTK_RESPONSE_CANCEL,
        NULL);

    /* Création de la zone de saisie */
    pEntry = gtk_entry_new();
    gtk_entry_set_text(GTK_ENTRY(pEntry), "Saisissez votre nom");
    /* Insertion de la zone de saisie dans la boite de dialogue */
    /* Rappel : paramètre 1 de gtk_box_pack_start de type GtkBox */
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(pBoite)->vbox), pEntry, TRUE, FALSE, 0);

    /* Affichage des éléments de la boite de dialogue */
    gtk_widget_show_all(GTK_DIALOG(pBoite)->vbox);

    /* On lance la boite de dialogue et on récupéré la réponse */
    switch (gtk_dialog_run(GTK_DIALOG(pBoite)))
    {
        /* L utilisateur valide */
        case GTK_RESPONSE_OK:
            sNom = gtk_entry_get_text(GTK_ENTRY(pEntry));
            gtk_label_set_text(GTK_LABEL(pLabel), sNom);
            break;
        /* L utilisateur annule */
        case GTK_RESPONSE_CANCEL:
        case GTK_RESPONSE_NONE:
        default:
            gtk_label_set_text(GTK_LABEL(pLabel), "Vous n'avez rien saisi !");
            break;
    }

    /* Destruction de la boite de dialogue */
    gtk_widget_destroy(pBoite);
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XII-B. Les boîtes de messages

    Comme nous venons de le voir, le widget GtkDialog permet d'accélérer la programmation de simple fenêtre de saisie. Pour ce qui est de l'affichage de message, GTK+ offre un nouveau widget qui dérive directement de GtkDialog : le widget GtkMessageDialog. Ce widget permet de créer une boîte de dialogue complète avec une seule fonction.

    Image non disponible

    XII-B-1. Création

    L'unique fonction de ce widget est la fonction permettant de créer la boîte de dialogue :

    Sélectionnez
    GtkWidget* gtk_message_dialog_new(GtkWindow *parent, GtkDialogFlags flags, GtkMessageType type, GtkButtonsType buttons, const gchar *message_format, ...);

    Les paramètres parent et flags sont identiques à ceux du widget GtkDialog, nous ne reviendrons donc pas dessus et allons-nous concentrer sur les nouveaux paramètres.

    Le tout premier type, permet de définir le texte qui sera affiché dans la barre de titre de la boîte de dialogue ainsi que l'icône correspondant. Ce paramètre est de type GtkMessageType et peut prendre une des quatre valeurs suivantes :

    Valeur | Titre de la boîte de dialogue | Icône
GTK_MESSAGE_INFO | Information | Image non disponible
GTK_MESSAGE_WARNING | Avertissement | Image non disponible
GTK_MESSAGE_QUESTION | Question | Image non disponible
GTK_MESSAGE_ERROR | Erreur | Image non disponible

    Le deuxième nouveau paramètre buttons permet de définir les boutons qui seront présents en bas de la boîte de dialogue. Les valeurs autorisées sont les suivantes :

    Macro | Les boutons | Valeur de retour
GTK_BUTTONS_NONE | Aucun bouton | Néant
GTK_BUTTONS_OK | Un bouton Ok | GTK_RESPONSE_OK
GTK_BUTTONS_CLOSE | Un bouton Fermer | GTK_RESPONSE_CLOSE
GTK_BUTTONS_CANCEL | Un bouton Annuler | GTK_RESPONSE_CANCEL
GTK_BUTTONS_YES_NO | Un bouton Oui Un bouton Non | GTK_RESPONSE_YES GTK_RESPONSE_NO
GTK_BUTTONS_OK_CANCEL | Un bouton Ok Un bouton Annuler | GTK_RESPONSE_OK GTK_RESPONSE_CANCEL

    Et le dernier paramètre message_format est tout simplement le texte qui sera affiché à l'intérieur de la boîte de dialogue. Ce texte peut être formaté comme il est possible de le faire avec la fonction printf.

    XII-B-2. Exemple

    Nous allons créer une fenêtre comportant deux boutons. Le premier permettra d'afficher les informations habituelles d'une boîte de dialogue « À propos… ». Le deuxième offrira la possibilité de quitter le programme, en passant par une demande de confirmation à l'utilisateur.

    XII-B-3. Programme exemple

    Sélectionnez
    #include <stdlib.h>
#include <gtk/gtk.h>

void on_about_btn(GtkWidget *pBtn, gpointer data);
void on_quitter_btn(GtkWidget *pBtn, gpointer data);

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pQuitterBtn;
    GtkWidget *pAboutBtn;

    gtk_init(&argc,&argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkMessageDialog");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(TRUE,0);
    gtk_container_add(GTK_CONTAINER(pWindow),pVBox);

    pAboutBtn = gtk_button_new_with_label("A propos...");
    gtk_box_pack_start(GTK_BOX(pVBox), pAboutBtn, TRUE, FALSE,0);
    g_signal_connect(G_OBJECT(pAboutBtn), "clicked", G_CALLBACK(on_about_btn), (GtkWidget*) pWindow);

    pQuitterBtn = gtk_button_new_from_stock (GTK_STOCK_QUIT);
    gtk_box_pack_start(GTK_BOX(pVBox), pQuitterBtn, TRUE, FALSE, 0);
    g_signal_connect(G_OBJECT(pQuitterBtn), "clicked", G_CALLBACK(on_quitter_btn), (GtkWidget*) pWindow);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

void on_about_btn(GtkWidget *pBtn, gpointer data)
{
    GtkWidget *pAbout;
    gchar *sSite = "http://gtk.developpez.com";

    /* Création de la boite de message */
    /* Type : Information -> GTK_MESSAGE_INFO */
    /* Bouton : 1 OK -> GTK_BUTTONS_OK */
    pAbout = gtk_message_dialog_new (GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        "Cours GTK+ 2.0\n%s",
        sSite);

    /* Affichage de la boite de message */
    gtk_dialog_run(GTK_DIALOG(pAbout));

    /* Destruction de la boite de message */
    gtk_widget_destroy(pAbout);
}

void on_quitter_btn(GtkWidget* widget, gpointer data)
{
    GtkWidget *pQuestion;

    /* Création de la boite de message */
    /* Type : Question -> GTK_MESSAGE_QUESTION */
    /* Boutons : 1 OUI, 1 NON -> GTK_BUTTONS_YES_NO */
    pQuestion = gtk_message_dialog_new (GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_QUESTION,
        GTK_BUTTONS_YES_NO,
        "Voulez-vous vraiment\nquitter ce programme?");

    /* Affichage et attente d une réponse */
    switch(gtk_dialog_run(GTK_DIALOG(pQuestion)))
    {
        case GTK_RESPONSE_YES:
            /* OUI -> on quitte l application */
            gtk_main_quit();
            break;
        case GTK_RESPONSE_NO:
            /* NON -> on détruit la boite de message */
            gtk_widget_destroy(pQuestion);
            break;
    }
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XII-C. En savoir plus

    XII-C-1. Fonctions documentées

    Sélectionnez
    GtkWidget* gtk_dialog_new();

    Permet de créer une boîte de dialogue vierge.

    Entrée(s) : rien.

    Sortie : pointeur sur la boîte de dialogue.

    Sélectionnez
    void gtk_dialog_add_buttons(GtkDialog *dialog, const gchar *first_button_text, ...);

    Ajoute des boutons à la boîte de dialogue.

    Entrée(s) :

    • dialog : la boîte de dialogue.
    • first_button_text… : texte et réponse des boutons.

    Sortie : rien.

    Sélectionnez
    GtkWidget* gtk_dialog_add_button(GtkDialog *dialog, const gchar *button_text, gint response_id);

    Ajoute un bouton à la boîte de dialogue.

    Entrée(s) :

    • dialog : la boîte de dialogue.
    • button_text : texte du bouton.
    • response_id : valeur de retour du bouton .

    Sortie : pointeur sur le nouveau bouton.

    Sélectionnez
    gboolean gtk_dialog_get_has_separator(GtkDialog *dialog);

    Pour savoir si la boîte de dialogue possède une barre de séparation.

    Entrée(s) :

    • dialog : la boîte de dialogue.

    Sortie : TRUE si la barre de séparation est présente, FALSE sinon.

    Sélectionnez
    void gtk_dialog_set_has_separator(GtkDialog *dialog, gboolean setting);

    Définit si la boîte de dialogue possède une barre de séparation

    Entrée(s) :

    • dialog : la boîte de dialogue en question
    • setting : TRUE pour l'ajouter, FALSE sinon.

    Sortie : rien.

    XII-C-2. Fonctions non documentées

    Sélectionnez
    void gtk_dialog_response(GtkDialog *dialog, gint response_id);
void gtk_dialog_set_default_response(GtkDialog *dialog, gint response_id);
void gtk_dialog_set_response_sensitive(GtkDialog *dialog, gint response_id, gboolean setting);
void gtk_dialog_add_action_widget(GtkDialog *dialog, GtkWidget *child, gint response_id);


Cours GTK 2

Table des matières

  • XIII. Les boutons (partie 2)
    • XIII-A. Le widget GtkToggleButton
      • XIII-A-1. Création du bouton
      • XIII-A-2. États du bouton
      • XIII-A-3. Exemple
      • XIII-A-4. 1.4 Programme exemple
    • XIII-B. Le widget GtkCheckButton
      • XIII-B-1. Création d'un GtkCheckButton
      • XIII-B-2. Utilisation d'un GtkCheckButton
    • XIII-C. Le widget GtkRadioButton
      • XIII-C-1. Création d'un groupe de GtkRadioButton.
      • XIII-C-2. Exemple
      • XIII-C-3. Programme exemple
    • XIII-D. En savoir plus
      • XIII-D-1. Fonctions documentées

    • XIII. Les boutons (partie 2)

    Nous allons étudier cette fois-ci trois nouveaux types de boutons qui dérivent du widget GtkButton (chapitre VLes boutons (partie 1)). L'étude de ces widgets sera rapide car ils ne comportent que très peu de fonctions.

    XIII-A. Le widget GtkToggleButton

    Il s'agit ici d'un bouton poussoir qui ne peut prendre que deux états : enfoncé ou relâché. Ce widget dérive de GtkButton.

    Image non disponible

    XIII-A-1. Création du bouton

    Vous allez voir ici, il n'y a rien de bien compliqué vu que c'est toujours le même principe :

    GtkWidget* gtk_toggle_button_new(void);
GtkWidget* gtk_toggle_button_new_with_label(const gchar* label);
GtkWidget* gtk_toggle_button_new_with_mnemonics(const gchar* label);

    La première fonction crée un nouveau bouton vide, alors que la seconde ajoute du texte à l'intérieur et la troisième ajoute en plus un raccourci clavier.

    XIII-A-2. États du bouton

    Il peut être intéressant de connaître l'état du bouton pour agir en conséquence. Une fois encore, rien de plus simple on utilise la fonction :

    gboolean gtk_toggle_button_get_active(GtkToggleButton *toggle_button);

    Cette dernière nous renvoie TRUE si le bouton est enfoncé et FALSE sinon. Afin de pouvoir utiliser le paramètre toggle_button qui est le bouton dont on veut connaître l'état, il faut utiliser la macro GTK_TOGGLE_BUTTON().

    Pour modifier l'état du bouton, c'est aussi simple :

    void gtk_toggle_button_set_active(GtkToggleButton *toggle_button, gboolean is_active);

    Il suffit de mettre le paramètre is_active à TRUE si l'on veut enfoncer le bouton ou à FALSE pour le relâcher.

    Il existe cependant un troisième état qui n'est pas accessible en cliquant sur le bouton mais par une fonction spécifique. Ce troisième état, vous le connaissez sûrement. Le meilleur exemple est celui des éditeurs de texte :

    Vous avez une phrase dans laquelle il y a du texte normal et du texte en gras. Si vous le texte en gras, le bouton permettant justement de le mettre en gras s'enfonce (en général B). Par contre si vous le texte normal, ce même bouton repasse à son état relâché. Venons en au troisième état, qui apparaît lorsque vous la phrase entière. Le bouton ne change pas d'état mais seulement d'aspect, il donne l'impression d'être inactif.

    Ce changement d'aspect doit se faire manuellement, et s'effectue avec cette fonction :

    void gtk_toggle_button_set_inconsistent(GtkToggleButton *toggle_button, gboolean setting);

    Il suffit de mettre le paramètre setting à TRUE pour donner au bouton l'aspect inactif.

    Et pour connaître l'aspect du bouton, il y a tout naturellement la fonction :

    gboolean gtk_toggle_button_get_inconsistent(GtkToggleButton *toggle_button);

    Évidemment, toutes les fonctions du widget GtkButton sont utilisables avec ce type de bouton.

    XIII-A-3. Exemple

    Nous allons construire une application contenant un GtkToggleButton qui changera d'état (bien sûr) lorsque nous cliquerons dessus, mais aussi lorsque nous cliquerons sur un deuxième bouton (le changement d'état entraînera la modification du label). De plus, il y aura un troisième bouton pour changer l'aspect du bouton (idem, on change aussi le label).

    La création des widgets étant classique, nous n'allons donc pas revenir dessus.

    Il va donc nous falloir trois fonctions callback. La première pour changer le texte lorsque l'on clique sur le GtkToggleButton, la deuxième pour changer son état lorsque l'on clique sur le deuxième bouton, et la troisième pour changer l'aspect.

    Pour la première fonction callback, il faut intercepter le signal « toggled » qui est émis lorsque le bouton change d'état :

    g_signal_connect(G_OBJECT(pToggleBtn), "toggled", G_CALLBACK(OnToggle), NULL);

    Dans cette fonction nous allons récupérer l'état du bouton ainsi que son aspect afin de changer le label en fonction des valeurs de retour.

    Pour la deuxième, on interceptera le signal « clicked » :

    g_signal_connect(G_OBJECT(pEtatBtn), "clicked", G_CALLBACK(OnEtatBtn), pToggleBtn);

    Lorsque l'on changera l'état du bouton toggle, le signal « toggled » sera émis pour celui-ci et le texte se changera automatiquement.

    Pour la troisième, le signal à intercepter et le même (« clicked ») :

    g_signal_connect(G_OBJECT(pAspectBtn), "clicked", G_CALLBACK(OnAspectBtn), pToggleBtn);

    Cette fois, lorsque l'on change l'aspect du bouton, le signal « toggled » n'est pas émis et le texte ne changera donc pas. Il faut donc émettre soit même ce signal avec la fonction :

    void gtk_toggle_button_toggled (GtkToggleButton *toggle_button);

    Cette fonction ne change en aucun cas l'état du bouton, elle ne fait qu'émettre le signal.

    Mais le plus simple est de regarder le code source suivant.

    XIII-A-4. 1.4 Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

void OnToggle(GtkWidget *pToggle, gpointer data);
void OnEtatBtn(GtkWidget *pWidget, gpointer pToggle);
void OnAspectBtn(GtkWidget *pWidget, gpointer pToggle);

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pToggleBtn;
    GtkWidget *pEtatBtn;
    GtkWidget *pAspectBtn;
    GtkWidget *pVBox;
    gchar *sLabel;

    gtk_init(&argc,&argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkToggleButton");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);

    pVBox = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    /* Création du label du bouton */
    sLabel = g_locale_to_utf8("Etat : Relâché - Aspect : Normal", -1, NULL, NULL, NULL);
    /* Création du bouton GtkToggleButton */
    pToggleBtn = gtk_toggle_button_new_with_label(sLabel);
    /* Le label sLabel n'est plus utile */
    g_free(sLabel);

    gtk_box_pack_start(GTK_BOX(pVBox), pToggleBtn, FALSE, FALSE, 0);

    pEtatBtn = gtk_button_new_with_label("CHANGER ETAT");
    gtk_box_pack_start(GTK_BOX(pVBox), pEtatBtn, FALSE, FALSE, 0);

    pAspectBtn = gtk_button_new_with_label("CHANGER ASPECT");
    gtk_box_pack_start(GTK_BOX(pVBox), pAspectBtn, FALSE, FALSE, 0);

    gtk_widget_show_all(pWindow);

    /* Connexion des signaux */
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(pToggleBtn), "toggled", G_CALLBACK(OnToggle), NULL);
    g_signal_connect(G_OBJECT(pEtatBtn), "clicked", G_CALLBACK(OnEtatBtn), pToggleBtn);
    g_signal_connect(G_OBJECT(pAspectBtn), "clicked", G_CALLBACK(OnAspectBtn), pToggleBtn);

    gtk_main();

    return EXIT_SUCCESS;
}

void OnEtatBtn(GtkWidget *pWidget, gpointer pToggle)
{
    gboolean bEtat;

    /* Recuperation de l'état du bouton */
    bEtat = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pToggle));

    /* Modification de l'état du bouton */
    gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(pToggle), (bEtat ^ TRUE));
}

void OnAspectBtn(GtkWidget *pEtatBtn, gpointer pToggle)
{
    gboolean bInconsistent;

    /* Récupération de l aspect du bouton */
    bInconsistent = gtk_toggle_button_get_inconsistent(GTK_TOGGLE_BUTTON(pToggle));

    /* Modification de l'aspect du bouton */
    gtk_toggle_button_set_inconsistent(GTK_TOGGLE_BUTTON(pToggle), (bInconsistent ^ TRUE));

    /* On émet le signal "toggle" pour changer le texte du bouton */
    gtk_toggle_button_toggled(GTK_TOGGLE_BUTTON(pToggle));
}

void OnToggle(GtkWidget *pToggle, gpointer data)
{
    gboolean bEtat;
    gboolean bInconsistent;
    gchar *sLabel;
    gchar *sLabelUtf8;

    /* Récupération de l'état du bouton */
    bEtat = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pToggle));
    /* Recuperation de l aspect du bouton */
    bInconsistent = gtk_toggle_button_get_inconsistent(GTK_TOGGLE_BUTTON(pToggle));

    /* Construction du label du bouton */
    sLabel = g_strdup_printf("Etat : %s - Aspect : %s",
        bEtat ? "Enfoncé" : "Relâché",
        bInconsistent ? "Modifié" : "Normal");
    /* Encodage du label en UTF8 */
    sLabelUtf8 = g_locale_to_utf8(sLabel, -1, NULL, NULL, NULL);

    /* Modification du label du bouton */
    gtk_button_set_label(GTK_BUTTON(pToggle), sLabelUtf8);

    /* Les chaînes sLabel et sLabelUtf8 n'ont plus d'utilité */
    g_free(sLabel);
    g_free(sLabelUtf8);
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XIII-B. Le widget GtkCheckButton

    Sous ce nom se cachent tout simplement les cases à cocher que tout le monde connaît. Il s'agit là aussi d'un type de bouton binaire, d'ailleurs ce widget dérive de GtkToggleButton :

    Image non disponible

    XIII-B-1. Création d'un GtkCheckButton

    Une fois encore, la syntaxe et l'utilisation des fonctions de création restent classiques :

    GtkWidget* gtk_check_button_new(void);
GtkWidget* gtk_check_button_new_with_label(const gchar *label);
GtkWidget* gtk_check_button_new_with_mnemonic(const gchar *label);

    XIII-B-2. Utilisation d'un GtkCheckButton

    Il n'existe pas d'autres fonctions pour ce widget, il faut donc utiliser les fonctions des widgets GtkToggleButton et GtkButton pour récupérer les propriétés du widget (état, aspect, label…).

    Nous n'allons pas ici créer de programme exemple pour ce widget car il suffit juste de remplacer gtk_toggle_button_new_with_label par gtk_check_button_new_with_label dans l'exemple précédent pour obtenir ceci :

    Image non disponible
    Image non disponible
    Image non disponible

    XIII-C. Le widget GtkRadioButton

    Nous passons maintenant au widget GtkRadioButton qui se différencie par la possibilité d'en grouper plusieurs. De ce fait, lors que par exemple nous avons un groupe de trois boutons, il n'y en a qu'un seul qui peut être actif. On pourrait très bien faire cela avec le widget GtkCheckButton mais cela serait beaucoup plus long à programmer. Dans la hiérarchie des widgets, GtkRadioButton dérive de GtkCheckButton.

    Image non disponible

    XIII-C-1. Création d'un groupe de GtkRadioButton.

    Afin de grouper les boutons radios, GTK+ utilise les listes simplement chaînées de GLib. Pour créer le premier bouton radio du groupe, il faut obligatoirement passer par une de ces fonctions :

    GtkWidget* gtk_radio_button_new(GSList *group);
GtkWidget* gtk_radio_button_new_with_label(GSList *group, const gchar *label);
GtkWidget* gtk_radio_button_new_with_mnemonic(GSList *group, const gchar *label);

    Au moment de la création, le bouton radio est automatiquement ajouté à la liste group. Cependant, ce paramètre n'est pas obligatoire. Nous pouvons très bien mettre NULL comme valeur et cela marchera de la même manière, sauf que nous n'aurons pas de pointeur sur la liste. La valeur de retour de cette fonction sera aussi copiée dans la variable data de la liste chaînée.

    Ensuite pour rajouter les autres boutons au groupe, il y a plusieurs possibilités. La première est d'utiliser une des trois fonctions précédentes mais ce n'est pas tout, car autant pour le premier bouton du groupe, il n'est pas nécessaire d'avoir une liste, autant pour les autres boutons cela devient obligatoire. Pour cela, GTK+ nous fournit une fonction qui permet d'obtenir la liste dans laquelle les boutons du groupe sont ajoutés :

    GSList* gtk_radio_button_get_group(GtkRadioButton *radio_button);

    Avec cette fonction, nous pouvons donc connaître la liste à laquelle appartient le bouton radio_button, ce qui va nous permettre d'ajouter de nouveau bouton au groupe. Mais là où cela se complique, c'est qu'il faut récupérer la liste avant chaque ajout de bouton avec le dernier bouton ajouté comme paramètre. Voici ce que cela donnerai pour un groupe de trois boutons :

    pRadio1 = gtk_radio_button_new_with_label(NULL, "Radio 1");
pGroup = gtk_radio_button_get_group(GTK_RADIO_BUTTON(pRadio1));
pRadio2 = gtk_radio_button_new_with_label(pGroup, "Radio 2");
pGroup = gtk_radio_button_get_group(GTK_RADIO_BUTTON(pRadio2));
pRadio3 = gtk_radio_button_new_with_label(pGroup, "Radio 3");

    Ce système peut donc s'avérer lourd lors de la création du groupe, surtout s'il contient un grand nombre de boutons. Heureusement, les concepteurs de GTK+ ont pensé à nous simplifier la vie en ajoutant ces trois fonctions :

    GtkWidget* gtk_radio_button_new_from_widget(GtkRadioButton *group);
GtkWidget* gtk_radio_button_new_with_label_from_widget(GtkRadioButton *group, const gchar *label);
GtkWidget* gtk_radio_button_new_with_mnemonic_from_widget(GtkRadioButton *group, const gchar *label);

    Cette fois group ne correspond pas à la liste mais à un des boutons du groupe. A chaque appel d'une de ces fonctions GTK+ va s'occuper de récupérer correctement la liste à laquelle appartient le bouton group et ajouter le nouveau bouton. Cela aura pour conséquence, dans le cas d'un groupe de trois boutons, de réduire le nombre de lignes de code de 5 à 3 (et oui, une ligne de code c'est une ligne de code).

    Nous savons maintenant tout ce qu'il faut pour créer un groupe de GtkRadioButton.

    XIII-C-2. Exemple

    Le programme exemple se présente sous la forme d'un mini sondage à trois choix :

    • Pour ;
    • Contre ;
    • Sans opinion.

    La possibilité de choisir parmi une de ces valeurs est rendue possible par l'utilisation des GtkRadioButton. Un autre bouton (tout ce qu'il y a de plus normal) permet de valider le choix fait par l'utilisateur ainsi que d'afficher le résultat dans une boîte de dialogue.

    XIII-C-3. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

void OnValider(GtkWidget *pBtn, gpointer data);

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pRadio1;
    GtkWidget *pRadio2;
    GtkWidget *pRadio3;
    GtkWidget *pValider;
    GtkWidget *pLabel;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkRadioButton");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);

    pVBox = gtk_vbox_new(TRUE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow),pVBox);

    pLabel = gtk_label_new("Votre choix :");
    gtk_box_pack_start(GTK_BOX(pVBox), pLabel, FALSE, FALSE, 0);

    /* Création du premier bouton radio */
    pRadio1 = gtk_radio_button_new_with_label(NULL, "Pour");
    gtk_box_pack_start(GTK_BOX (pVBox), pRadio1, FALSE, FALSE, 0);
    /* Ajout du deuxieme */
    pRadio2 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON (pRadio1), "Contre");
    gtk_box_pack_start(GTK_BOX (pVBox), pRadio2, FALSE, FALSE, 0);
    /* Ajout du troisième */
    pRadio3 = gtk_radio_button_new_with_label_from_widget(GTK_RADIO_BUTTON (pRadio1), "Sans opinion");
    gtk_box_pack_start(GTK_BOX (pVBox), pRadio3, FALSE, FALSE, 0);

    pValider = gtk_button_new_from_stock(GTK_STOCK_OK);
    gtk_box_pack_start(GTK_BOX (pVBox), pValider, FALSE, FALSE, 0);

    gtk_widget_show_all(pWindow);

    /* Connexion des signaux */
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    g_signal_connect(G_OBJECT(pValider), "clicked", G_CALLBACK(OnValider), pRadio1);

    gtk_main();

    return EXIT_SUCCESS;
}

void OnValider(GtkWidget *pBtn, gpointer data)
{
    GtkWidget *pInfo;
    GtkWidget *pWindow;
    GSList *pList;
    const gchar *sLabel;

    /* Récupération de la liste des boutons */
    pList = gtk_radio_button_get_group(GTK_RADIO_BUTTON(data));

    /* Parcours de la liste */
    while(pList)
    {
        /* Le bouton est-il sélectionné */
        if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(pList->data)))
        {
            /* OUI -> on copie le label du bouton */
            sLabel = gtk_button_get_label(GTK_BUTTON(pList->data));
            /* On met la liste a NULL pour sortir de la boucle */
            pList = NULL;
        }
        else
        {
            /* NON -> on passe au bouton suivant */
            pList = g_slist_next(pList);
        }
    }

    /* On récupére la fenêtre principale */
    /*
    /* A partir d'un widget, gtk_widget_get_toplevel
    /* remonte jusqu'à la fenêtre mère qui nous est
    /* utile pour l'affichage de la boite de dialogue
    */
    pWindow = gtk_widget_get_toplevel(GTK_WIDGET(data));

    pInfo = gtk_message_dialog_new(GTK_WINDOW(pWindow),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        "Vous avez choisi : %s", sLabel);

    gtk_dialog_run(GTK_DIALOG(pInfo));

    gtk_widget_destroy(pInfo);
}

    Résultat :

    Image non disponible
    Image non disponible

    XIII-D. En savoir plus

    XIII-D-1. Fonctions documentées

    GtkToggleButton

    void gtk_toggle_button_set_mode(GtkToggleButton *toggle_button, gboolean draw_indicator);

    Définit si le bouton est visible ou pas.

    Entrée(s) :

    • toggle_button : le bouton.
    • draw_indicator : TRUE pour visible, FALSE pour invisible.

    Sortie : rien

    gboolean gtk_toggle_button_get_mode(GtkToggleButton *toggle_button);

    Pour savoir si le bouton est visible ou pas.

    Entrée(s) :

    • toggle_button : le bouton.

    Sortie : TRUE si le bouton est visible, FALSE sinon.

    GtkRadioButton

    void gtk_radio_button_set_group(GtkRadioButton *radio_button, GSList *group);

    Pour changer un bouton radio de groupe.

    Entrée(s) :

    • toggle_button : le bouton.
    • group : le nouveau groupe du bouton.

    Sortie : rien


Cours GTK 2

Table des matières

  • XIV. Les menus
    • XIV-A. Le menu - Méthode longue.
      • XIV-A-1. Les éléments d'un menu
      • XIV-A-2. Création d'un menu
      • XIV-A-3. Exemple
      • XIV-A-4. 1.4 Programme exemple
    • XIV-B. Éléments « avancés » de menu.
      • XIV-B-1. Les GtkImageMenuItem
      • XIV-B-2. Les GtkCheckMenuItem
      • XIV-B-3. Les GtkRadioMenuItem
      • XIV-B-4. Les GtkSeparatorMenuItem
      • XIV-B-5. Les GtkTearoffMenuItem
      • XIV-B-6. Programme exemple
    • XIV-C. Le menu - Méthode rapide
      • XIV-C-1. Fonctionnement de GtkItemFactory
      • XIV-C-2. Création des éléments du menu
      • XIV-C-3. Création du menu
      • XIV-C-4. 3.4 Exemple.
      • XIV-C-5. 3.5 Programme exemple.
    • XIV-D. En savoir plus
      • XIV-D-1. Les signaux
      • XIV-D-2. Fonctions non documentées

    • XIV. Les menus

    Nous allons cette fois voir quelles sont les solutions que GTK+ met à notre disposition pour ajouter un menu à une fenêtre. Nous parlons de « solutions » au pluriel car il existe en fait deux méthodes possibles à la création d'un menu, l'une étant plus rapide que l'autre.

    Ce chapitre est composé de trois parties distinctes :

    • Une première partie traitant de la création d'un menu classique par la méthode la plus longue ;
    • Une deuxième partie dans laquelle nous verrons des éléments spéciaux d'un menu ;
    • La troisième partie abordera la création d'un menu par la méthode rapide.

    XIV-A. Le menu - Méthode longue.

    Pour construire un tel menu, nous allons utiliser pas moins de trois widgets différents qui serviront tous à différentes étapes de la création. Ces widgets sont GtkMenuBar, GtkMenu, GtkMenuItem dont voici leurs positions dans la hiérarchie GTK+ :

    Image non disponible

    XIV-A-1. Les éléments d'un menu

    Dans un premier temps, le plus simple est de voir comment seront disposés ces trois widgets dans notre futur menu.

    Image non disponible

    Nous avons tous d'abord le widget GtkMenuBar (en rouge) qui est l'élément principal de notre menu. Il contient des éléments de type GtkMenuItem (en bleu) qui peuvent ouvrir des éléments GtkMenu (en vert) contenant aussi des GtkMenuItem.

    Du point de vue du fonctionnement, les éléments GtkMenuItem peuvent soit ouvrir un sous-menu (élément GtkMenu) soit exécuter l'action qui lui est associée par l'intermédiaire d'une fonction callback. Il ne reste maintenant plus qu'à créer notre menu.

    XIV-A-2. Création d'un menu

    La création d'un menu doit passer par au moins six étapes différentes :

    • Étape 1 : création de l'élément GtkMenuBar qui sera la barre de menu;
    • Étape 2 : création d'un élément GtkMenu qui sera un menu ;
    • Étape 3 : création des éléments GtkMenuItem à insérer dans le menu ;
    • Étape 4 : création de l'élément GtkMenuItem qui ira dans l'élément GtkMenuBar ;
    • Étape 5 : association de cet élément avec le menu créer précédemment ;
    • Étape 6 : ajout de l'élément GtkMenuItem dans la barre GtkMenuBar.

    Si par la suite, vous souhaitez ajouter d'autres menus, il suffit de recommencer à partir de l'étape 2. Étudions maintenant les fonctions qui vont nous permettre de coder le menu.

    Étape 1 : création de l'élément GtkMenuBar

    • Cette étape, rapide et simple, se résume en l'utilisation d'une seule fonction :

      GtkWidget* gtk_menu_bar_new(void);

    Étape 2 : création d'un élément GtkMenu qui sera un menu

    • Cette fois aussi, une seule fonction est utilisée :

      GtkWidget* gtk_menu_new(void);

    Étape 3 : création des éléments GtkMenuItem à insérer dans le menu

    • Dans un premier temps, il faut créer le GtkMenuItem grâce à l'une de ces trois fonctions :

      GtkWidget* gtk_menu_item_new(void);
GtkWidget* gtk_menu_item_new_with_label(const gchar* label);
GtkWidget* gtk_menu_item_new_with_mnemonic(const gchar* label);

      Nous reconnaissons la syntaxe de ces constructeurs qui est semblable à celles des boutons. La première fonction créer un élément vide, la deuxième un élément avec un texte (label), et la troisième un élément avec un texte associé à un raccourci.

      Maintenant que l'élément est créé, il faut l'insérer dans le menu. Pour cela, il existe plusieurs fonctions possibles, mais nous n'allons en voir que deux :

      void gtk_menu_shell_append(GtkMenuShell *menu_shell, GtkWidget *child);
void gtk_menu_shell_prepend(GtkMenuShell *menu_shell, GtkWidget *child);

      Ces fonctions ne font pas partie du widget GtkMenu mais de GtkMenuShell qui est le widget dont GtkMenu dérive. La première fonction ajoute les éléments de haut en bas alors que la seconde les ajoute de bas en haut. Le paramètre menu_shell est le menu dans lequel nous voulons ajouter l'élément et le paramètre child est l'élément à ajouter. Pour le premier paramètre, il faudra utiliser la macro de conversion GTK_MENU_SHELL() .

      Cette étape peut s'avérer longue à coder, car il faudra la répéter pour chaque élément que nous voulons ajouter au menu.

    Étape 4 : création de l'élément GtkMenuItem qui ira dans l'élément GtkMenuBar

    • Il suffit d'utiliser une des trois fonctions de création du widget GtkMenuItem.

    Étape 5 : association de cet élément avec le menu créer précédemment

    • Il faut maintenant dire que lorsque l'utilisateur cliquera sur l'élément créer pendant l'étape 4, il faudra ouvrir le menu qui a été créé précédemment. La fonction adéquate est celle-ci :

      void gtk_menu_item_set_submenu(GtkMenuItem *menu_item, GtkWidget *submenu);

      Cette fonction va donc associer le GtkMenuItem menu_item au GtkMenu submenu. Comme d'habitude, il faudra convertir le premier paramètre, cette fois-ci à l'aide de la macro GTK_MENU_ITEM() .

    Étape 6 : ajout de l'élément GtkMenuItem dans la barre GtkMenuBar

    • GtkMenuBar dérivant aussi de GtkMenuShell, nous allons utiliser la même fonction que lors de l'étape 3 ( gtk_menu_shell_append ) pour ajouter le sous-menu au menu principal.

    XIV-A-3. Exemple

    Notre exemple comportera deux menus. Le premier « Fichier », proposera des fonctions inactives (« Nouveau », « Ouvrir », « Enregistrer », « Fermer ») et une fonction active (« Quitter »), alors que le second « ? » proposera la fonction « À propos de… ».

    XIV-A-4. 1.4 Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>
void OnQuitter(GtkWidget* widget, gpointer data);
void OnAbout(GtkWidget* widget, gpointer data);
int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pMenuBar;
    GtkWidget *pMenu;
    GtkWidget *pMenuItem;
    gtk_init(&argc, &argv);
    /* Création de la fenêtre */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkMenu");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    /* Création de la GtkVBox */
    pVBox = gtk_vbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);
    /**** Création du menu ****/
    /* ETAPE 1 */
    pMenuBar = gtk_menu_bar_new();
    /** Premier sous-menu **/
    /* ETAPE 2 */
    pMenu = gtk_menu_new();
    /* ETAPE 3 */
    pMenuItem = gtk_menu_item_new_with_label("Nouveau");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_menu_item_new_with_label("Ouvrir");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_menu_item_new_with_label("Enregistrer");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_menu_item_new_with_label("Fermer");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_menu_item_new_with_label("Quitter");
    g_signal_connect(G_OBJECT(pMenuItem), "activate", G_CALLBACK(OnQuitter),(GtkWidget*) pWindow);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    /* ETAPE 4 */
    pMenuItem = gtk_menu_item_new_with_label("Fichier");
    /* ETAPE 5 */
    gtk_menu_item_set_submenu(GTK_MENU_ITEM(pMenuItem), pMenu);
    /* ETAPE 6 */
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenuBar), pMenuItem);
    /** Second sous-menu **/
    /* ETAPE 2 */
    pMenu = gtk_menu_new();
    /* ETAPE 3 */
    pMenuItem = gtk_menu_item_new_with_label("A propos de...");
    g_signal_connect(G_OBJECT(pMenuItem), "activate", G_CALLBACK(OnAbout),(GtkWidget*) pWindow);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    /* ETAPE 4 */
    pMenuItem = gtk_menu_item_new_with_label("?");
    /* ETAPE 5 */
    gtk_menu_item_set_submenu(GTK_MENU_ITEM(pMenuItem), pMenu);
    /* ETAPE 6 */
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenuBar), pMenuItem);
    /* Ajout du menu a la fenetre */
    gtk_box_pack_start(GTK_BOX(pVBox), pMenuBar, FALSE, FALSE, 0);
    gtk_widget_show_all(pWindow);
    gtk_main();
    return EXIT_SUCCESS;
}
void OnQuitter(GtkWidget* widget, gpointer data)
{
    GtkWidget *pQuestion;
    pQuestion = gtk_message_dialog_new(GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_QUESTION,
        GTK_BUTTONS_YES_NO,
        "Voulez vous vraiment\n"
        "quitter le programme?");
    switch(gtk_dialog_run(GTK_DIALOG(pQuestion)))
    {
        case GTK_RESPONSE_YES:
            gtk_main_quit();
            break;
        case GTK_RESPONSE_NONE:
        case GTK_RESPONSE_NO:
            gtk_widget_destroy(pQuestion);
            break;
    }
}
void OnAbout(GtkWidget* widget, gpointer data)
{
    GtkWidget *pAbout;
    pAbout = gtk_message_dialog_new(GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        "Cours GTK+ 2.0\n"
        "http://gtk.developpez.com");
    gtk_dialog_run(GTK_DIALOG(pAbout));
    gtk_widget_destroy(pAbout);
}

    Résultat :

    Image non disponible
    Image non disponible

    XIV-B. Éléments « avancés » de menu.

    En plus des GtkMenuItem, GTK+ offre cinq éléments de menu additionnels prêts à l'emploi : GtkImageMenuItem, GtkRadioMenuItem, GtkCheckMenuItem, GtkSeparatorMenuItem et GtkTearoffMenuItem. Bien entendu toutes les fonctions s'appliquant sur des GtkMenuItem s'appliquent aussi à ces items (héritage oblige).

    Image non disponible
    • GtkImageMenuItem est un widget qui permet de créer une entrée de menu textuelle avec une icône juste devant, par exemple :
    Image non disponible
    • GtkRadioMenuItem est un widget qui marche en groupe, un seul GtkRadioMenuItem d'un groupe peut être activé comme dans l'exemple ci dessous.
      « Groupe 1 : choix 1 » et « Groupe 1 : choix 2 » font partie d'un premier groupe,
      « Groupe 2 : choix 1 », « Groupe 2 : choix 2 » et « Groupe 2 : choix 3 » font partie d'un deuxième.
      Seule une entrée du premier groupe peut être activée (ici c'est « Groupe 1 : choix 1 ») et pareil pour le deuxième groupe (où l'entrée activée est « Groupe 2 : choix 1 ») :
    Image non disponible
    • GtkCheckMenuItem est un widget à trois états que l'on peut cocher :
    Image non disponible
    • GtkSeparatorMenuItem est un widget qui sert simplement à séparer des parties de menu gràce à une ligne horizontale. Leur but est uniquement décoratif :
    Image non disponible
    • GtkTearoffMenuItem est un widget qui permet de détacher le menu de sa barre et d'en faire une fenêtre à part entière :
    Image non disponible

    Le menu encore attaché (le GtkTearoffMenuItem c'est les pointillés en haut du menu)

    Image non disponible

    Le menu une fois détaché

    XIV-B-1. Les GtkImageMenuItem

    Pour créer un GtkImageMenuItem, on a à disposition quatre fonctions :

    GtkWidget* gtk_image_menu_item_new(void);
GtkWidget* gtk_image_menu_item_new_from_stock(const gchar *stock_id, GtkAccelGroup *accel_group);
GtkWidget* gtk_image_menu_item_new_with_label(const gchar *label);
GtkWidget* gtk_image_menu_item_new_with_mnemonic(const gchar *label);

    On retrouve encore les mêmes types de fonctions de création, aussi je passe sur l'explication des paramètres. La seule nouveautée est peut être le GtkAccelGroup *accel_group de gtk_image_menu_item_new_from_stock , qui sert à ajouter l'entrée à un GtkAccelGroup précédemment créé en utilisant le raccourci par défaut de l'icône stock.

    Maintenant, il s'agit de définir une icône pour notre entrée, pour cela, on a :

    void gtk_image_menu_item_set_image(GtkImageMenuItem *image_menu_item, GtkWidget *image);

    Cette fonction permet de définir l'icône (généralement on utilisera un GtkImage) qui sera affichée par l'entrée passée en paramètre. Cette fonction peut aussi servir à remplacer l'icône que gtk_image_menu_item_new_from_stock définit pour l'entrée lors de sa création.

    La dernière fonction spécifique des GtkImageMenuItem est :

    GtkWidget* gtk_image_menu_item_get_image(GtkImageMenuItem *image_menu_item);

    Elle sert, comme vous l'aurez deviné, à récupérer ce qui sert d'icône à l'entrée.

    XIV-B-2. Les GtkCheckMenuItem

    Les GtkCheckMenuItem émettent un signal lorsqu'on les (dé)coche, il s'agit du signal « toggled » et dont la fonction de rappel est de la forme

    void user_function(GtkCheckMenuItem *checkmenuitem, gpointer user_data);

    Les fonctions de création d'un GtkCheckMenuItem sont :

    GtkWidget* gtk_check_menu_item_new(void);
GtkWidget* gtk_check_menu_item_new_with_label(const gchar *label);
GtkWidget* gtk_check_menu_item_new_with_mnemonic(const gchar *label);

    Rien de nouveau à l'horizon, alors nous continuons.

    Nous avons plusieurs fonctions qui permettent de changer l'état de notre GtkCheckMenuItem par programme :

    void gtk_check_menu_item_set_active(GtkCheckMenuItem *check_menu_item, gboolean is_active);
void gtk_check_menu_item_set_inconsistent(GtkCheckMenuItem *check_menu_item, gboolean setting);
void gtk_check_menu_item_toggled(GtkCheckMenuItem *check_menu_item);

    La première fonction pemet de passer le GtkCheckMenuItem dans l'état « coché » si le paramètre is_active est TRUE ou dans l'état « non coché » si is_active est FALSE. Cette fonction ne permet pas de mettre notre GtkCheckMenuItem dans le troisième état « demi coché », pour cela, il faut utiliser la deuxième fonction, et grâce au paramètre setting , on active ou non cet état. La troisième fonction, elle, permet d'alterner entre état « coché » et « non coché » car elle émet en fait le signal « toggled », ce qui a pour effet d'inverser l'état du GtkCheckMenuItem.

    Nous disposons également des fonctions associées pour récupérer l'état d'un GtkCheckMenuItem :

    gboolean gtk_check_menu_item_get_active(GtkCheckMenuItem *check_menu_item);
gboolean gtk_check_menu_item_get_inconsistent(GtkCheckMenuItem *check_menu_item);

    La première permet de connaître l'état d'un GtkCheckMenuItem, elle renvoie TRUE s'il est « coché » ou FALSE sinon.

    La deuxième permet juste de savoir si le GtkCheckMenuItem est dans le troisième état « demi coché ».

    XIV-B-3. Les GtkRadioMenuItem

    Les GtkRadioMenuItem héritent des GtkCheckMenuItem, donc tout ce qui a été dit juste avant s'applique aussi ici.

    Pour créer un GtkRadioMenuItem, nous pouvons nous servir de :

    GtkWidget* gtk_radio_menu_item_new(GSList *group);
GtkWidget* gtk_radio_menu_item_new_with_label(GSList *group, const gchar *label);
GtkWidget* gtk_radio_menu_item_new_with_mnemonic(GSList *group, const gchar *label);

    Ce widget fonctionne de la même manière que le widget GtkRadioButton, et donc le paramètre group de ces fonctions, sert à dire au GtkRadioMenuItem à quel groupe il va appartenir.

    Le premier GtkRadioMenuItem d'un groupe prendra toujours NULL comme paramètre group, de cette façon il va en créer un nouveau. Ensuite, il suffira de récupérer ce paramètre group grâce à la fonction suivante et de le passer à un autre GtkRadioMenuItem pour que celui ci fasse partie du même groupe que le premier.

    GSList* gtk_radio_menu_item_get_group(GtkRadioMenuItem *radio_menu_item);

    Tout comme pour le widget GtkRadioButton, quand on crée un groupe de GtkRadioMenuItem, il faut toujours récupérer le groupe du GtkRadioMenuItem précédemment créé (sauf pour le premier bien entendu)

    On peut aussi définir ou remplacer le groupe d'un GtkRadioMenuItem aprés coup grâce à :

    void gtk_radio_menu_item_set_group(GtkRadioMenuItem *radio_menu_item, GSList *group);

    XIV-B-4. Les GtkSeparatorMenuItem

    Ah, en voilà un widget qu'il est bien !

    Une seule fonction pour sa création, et c'est tout :

    GtkWidget* gtk_separator_menu_item_new(void);

    Ça c'est des fonctions qu'on aime ;-)

    XIV-B-5. Les GtkTearoffMenuItem

    Pour le créer faites :

    GtkWidget* gtk_tearoff_menu_item_new(void);

    Et c'est tout ! Ensuite, un premier clic sur l'élément GtkTearoffMenuItem créera une copie du menu dans une fenêtre, et un second clic sur l'élément (que cela soit dans le menu ou dans la fenêtre) supprimera la fenêtre créée.

    Pour savoir si le menu est attaché ou détaché, il faut utiliser la fonction :

    gboolean gtk_menu_get_tearoff_state(GtkMenu *menu);

    Cette dernière renverra TRUE si le menu est détaché et FALSE sinon.

    Lorsque nous fermons la fenêtre du menu détaché à la main (c'est-à-dire lorsque nous cliquons sur la croix), le menu est automatiquement réattaché, mais gtk_menu_get_tearoff_state renverra tout de même TRUE.

    XIV-B-6. Programme exemple

    Nous avons repris l'exemple précédent en modifiant le menu pour y mettre nos nouveaux items et en y ajoutant trois labels pour indiquer l'état des différents items. Il y a aussi trois callbacks supplémentaires pour la mise à jour de nos labels.

    #include <stdlib.h>
#include <gtk/gtk.h>
void OnQuitter(GtkWidget* widget, gpointer data);
void OnAbout(GtkWidget* widget, gpointer data);
void OnRadio(GtkWidget* widget, gpointer data);
void OnTearoff(GtkWidget* widget, gpointer data);
void OnCheck(GtkWidget* widget, gpointer data);
static GtkWidget *pRadioLabel;
static GtkWidget *pCheckLabel;
static GtkWidget *pTearoffLabel;
int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pVBox2;
    GtkWidget *pMenuBar;
    GtkWidget *pMenu;
    GtkWidget *pMenuItem;
    GSList *pList;
    gchar *sTempLabel;
    gtk_init(&argc, &argv);
    /* Création de la fenêtre */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkMenu");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    /* Création de la GtkVBox */
    pVBox = gtk_vbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);
    /**** Création du menu ****/
    /* ETAPE 1 */
    pMenuBar = gtk_menu_bar_new();
    /** Premier sous-menu **/
    /* ETAPE 2 */
    pMenu = gtk_menu_new();
    /* ETAPE 3 */
    /* GtkTearoffMenuItem */
    pMenuItem = gtk_tearoff_menu_item_new();
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    g_signal_connect(G_OBJECT(pMenuItem),"activate",G_CALLBACK(OnTearoff),(gpointer)pMenu);
    /* GtkImageMenuItem */
    pMenuItem = gtk_image_menu_item_new_from_stock(GTK_STOCK_NEW,NULL);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_image_menu_item_new_from_stock(GTK_STOCK_OPEN,NULL);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_image_menu_item_new_from_stock(GTK_STOCK_SAVE,NULL);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_image_menu_item_new_from_stock(GTK_STOCK_CLOSE,NULL);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    /* GtkSeparatorItem */
    pMenuItem = gtk_separator_menu_item_new();
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    /* GtkRadioMenuItem */
    pMenuItem = gtk_radio_menu_item_new_with_label(NULL, "Radio 1");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pList = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(pMenuItem));
    /* Il est inutile ici d'utiliser le signal "toggled" */
    g_signal_connect(G_OBJECT(pMenuItem), "activate", G_CALLBACK(OnRadio), NULL);
    pMenuItem = gtk_radio_menu_item_new_with_label(pList, "Radio 2");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pList = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(pMenuItem));
    g_signal_connect(G_OBJECT(pMenuItem) ,"activate", G_CALLBACK(OnRadio), NULL);
    pMenuItem = gtk_radio_menu_item_new_with_label(pList, "Radio 3");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pList = gtk_radio_menu_item_get_group(GTK_RADIO_MENU_ITEM(pMenuItem));
    g_signal_connect(G_OBJECT(pMenuItem) ,"activate", G_CALLBACK(OnRadio), NULL);
    /* GtkSeparatorItem */
    pMenuItem = gtk_separator_menu_item_new();
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    /* GtkCheckMenuItem */
    pMenuItem = gtk_check_menu_item_new_with_label("Check");
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    g_signal_connect(G_OBJECT(pMenuItem),"toggled",G_CALLBACK(OnCheck),(gpointer)pMenu);
    /* GtkSeparatorItem */
    pMenuItem = gtk_separator_menu_item_new();
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    pMenuItem = gtk_image_menu_item_new_from_stock(GTK_STOCK_QUIT,NULL);
    g_signal_connect(G_OBJECT(pMenuItem), "activate", G_CALLBACK(OnQuitter), (GtkWidget*) pWindow);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    /* ETAPE 4 */
    pMenuItem = gtk_menu_item_new_with_label("Fichier");
    /* ETAPE 5 */
    gtk_menu_item_set_submenu(GTK_MENU_ITEM(pMenuItem), pMenu);
    /* ETAPE 6 */
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenuBar), pMenuItem);
    /** Deuxieme sous-menu **/
    /* ETAPE 2 */
    pMenu = gtk_menu_new();
    /* ETAPE 3 */
    pMenuItem = gtk_menu_item_new_with_label("A propos de...");
    g_signal_connect(G_OBJECT(pMenuItem), "activate", G_CALLBACK(OnAbout), (GtkWidget*) pWindow);
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenu), pMenuItem);
    /* ETAPE 4 */
    pMenuItem = gtk_menu_item_new_with_label("?");
    /* ETAPE 5 */
    gtk_menu_item_set_submenu(GTK_MENU_ITEM(pMenuItem), pMenu);
    /* ETAPE 6 */
    gtk_menu_shell_append(GTK_MENU_SHELL(pMenuBar), pMenuItem);
    /* Creation de la deuxieme GtkVBox (pour les labels) */
    pVBox2 = gtk_vbox_new(FALSE, 0);
    pRadioLabel = gtk_label_new("Radio 1 est actif");
    gtk_box_pack_start(GTK_BOX(pVBox2), pRadioLabel, TRUE, TRUE, 0);
    sTempLabel = g_locale_to_utf8("Check est décoché", -1, NULL, NULL, NULL);
    pCheckLabel = gtk_label_new(sTempLabel);
    g_free(sTempLabel);
    gtk_box_pack_start(GTK_BOX(pVBox2), pCheckLabel, TRUE, TRUE, 0);
    sTempLabel = g_locale_to_utf8("Menu attaché", -1, NULL, NULL, NULL);
    pTearoffLabel = gtk_label_new(sTempLabel);
    g_free(sTempLabel);
    gtk_box_pack_start(GTK_BOX(pVBox2), pTearoffLabel, TRUE, TRUE, 0);
    /* Ajout du menu a la fenêtre */
    gtk_box_pack_start(GTK_BOX(pVBox), pMenuBar, FALSE, FALSE, 0);
    /* Ajout des labels a la fenetre */
    gtk_box_pack_start(GTK_BOX(pVBox), pVBox2, TRUE, TRUE, 0);
    gtk_widget_show_all(pWindow);
    gtk_main();
    return EXIT_SUCCESS;
}
void OnRadio(GtkWidget* widget, gpointer data)
{
    const gchar *sRadioName;
    gchar *sLabel;
    /* Récupérer le label du bouton radio active */
    sRadioName = gtk_label_get_label(GTK_LABEL(GTK_BIN(widget)->child));
    sLabel = g_strdup_printf("%s est actif",sRadioName);
    gtk_label_set_label(GTK_LABEL(pRadioLabel), sLabel);
    g_free(sLabel);
}
void OnCheck(GtkWidget* widget, gpointer data)
{
    gboolean bCoche;
    gchar *sLabel;
    gchar *sLabelUtf8;
    /* Savoir si le GtkCheckMenuItem est coche ou non */
    bCoche = gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(widget));
    if(bCoche)
        sLabel = g_strdup("Check est coché");
    else
        sLabel = g_strdup("Check est décoché");
    sLabelUtf8 = g_locale_to_utf8(sLabel, -1, NULL, NULL, NULL);
    gtk_label_set_label(GTK_LABEL(pCheckLabel), sLabelUtf8);
    g_free(sLabel);
    g_free(sLabelUtf8);
}
void OnTearoff(GtkWidget* widget, gpointer data)
{
    gboolean bDetache;
    gchar *sLabel;
    gchar *sLabelUtf8;
    /* Savoir si le menu est détaché ou non */
     bDetache = gtk_menu_get_tearoff_state(GTK_MENU(data));
    if(bDetache)
        sLabel = g_strdup("Menu détaché");
    else
        sLabel = g_strdup("Menu attaché");
    sLabelUtf8 = g_locale_to_utf8(sLabel, -1, NULL, NULL, NULL);
    gtk_label_set_label(GTK_LABEL(pTearoffLabel), sLabelUtf8);
    g_free(sLabel);
    g_free(sLabelUtf8);
}
void OnQuitter(GtkWidget* widget, gpointer data)
{
    GtkWidget *pQuestion;
    pQuestion = gtk_message_dialog_new(GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_QUESTION,
        GTK_BUTTONS_YES_NO,
        "Voulez vous vraiment\n"
        "quitter le programme?");
    switch(gtk_dialog_run(GTK_DIALOG(pQuestion)))
    {
        case GTK_RESPONSE_YES:
            gtk_main_quit();
            break;
        case GTK_RESPONSE_NONE:
        case GTK_RESPONSE_NO:
            gtk_widget_destroy(pQuestion);
            break;
    }
}
void OnAbout(GtkWidget* widget, gpointer data)
{
    GtkWidget *pAbout;
    pAbout = gtk_message_dialog_new(GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        "Cours GTK+ 2.0\n"
        "http://gtk.developpez.com");
    gtk_dialog_run(GTK_DIALOG(pAbout));
    gtk_widget_destroy(pAbout);
}

    Résultat :

    Image non disponible
    Image non disponible

    XIV-C. Le menu - Méthode rapide

    Cette méthode est obsolète depuis GTK+ 2.4, il est fortement conseillé d'utiliser GtkUIManager en remplacement.

    Grâce aux deux premières parties du chapitre, nous avons appris à créer un menu. Vous avez pu remarquer que cette méthode peut s'avérer très fastidieuse si le menu comporte beaucoup d'éléments. Nous allons maintenant voir comment simplifier tout cela pour le biais de l'objet GtkItemFactory.

    Image non disponible

    XIV-C-1. Fonctionnement de GtkItemFactory

    L'objet GtkItemFactory n'est en fait qu'une grosse machine nous fournissant les fonctions nécessaires à la création d'un menu. Pour cela, GtkItemFactory utilise la structure GtkItemFactoryEntry qui elle nous permet de définir les différents éléments qui composent le menu ainsi que les actions qui leurs sont associés. Voici la définition de la structure GtkItemFactoryEntry :

    struct GtkItemFactoryEntry {
    gchar *path;
    gchar *accelerator;
    GtkItemFactoryCallback callback;
    guint callback_action;
    gchar *item_type;
    gconstpointer extra_data;
};

    XIV-C-2. Création des éléments du menu

    Pour cela, la seule à faire est de déclarer et de remplir un tableau de GtkItemFactoryEntry. Nous allons donc maintenant étudier comment remplir ce tableau d'après la définition de la structure GtkItemFactoryEntry.

    Le paramètre path correspond au chemin de l'élément dans l'arborescence du menu. Il faut pour cela, voir l'arborescence d'un menu comme celle des fichiers, c'est-à-dire organisée en répertoire (les branches du menu) et en fichier (les éléments du menu). Ce paramètre permet aussi de définir le texte de l'élément. Par exemple, nous avons un menu « Fichier » qui possède un élément « Nouveau », alors le path de la branche « Fichier » sera « /Fichier » et celui de l'élément « Nouveau » sera « /Fichier/Nouveau ». Pour souligner une lettre (pour indiquer un raccourci clavier), il faut précéder cette lettre de « _ » comme pour les mnémonics. Cependant, cela ne crée pas un raccourci, il s'agit juste d'une indication.

    Le second paramètre accelerator définit la combinaison de touche qui activera l'élément du menu, et cela, même si le menu n'est pas ouvert. La combinaison de touche allouée à l'élément est affichée après le label de l'élément du menu. La syntaxe de ce paramètre est « <touche_speciale>Lettre », touche_spéciale pouvant être Ctrl, Alt ou Shift. Voici donc le récapitulatif des solutions possibles :

    Valeur | Touche
<ALT> | Alt
<CTRL> <CONTROL> <CTL> | Control
<SHFT> <SHIFT> | Shift

    Il est bien sûr possible d'utiliser une combinaison de ces touches (exemple : « <CTRL><SHIFT>O »).

    Le paramètre callback est la fonction callback qui sera appelée lorsque l'élément sera activé à l'aide de la souris ou d'un raccourci clavier.

    Le quatrième paramètre, callback_action , a plusieurs incidences sur la fonction callback. Tout d'abord, si callback_action est égal à 0, la fonction callback devra avoir ce prototype :

    void fonction_callback(void);

    Dans tous les autres cas, le prototype sera :

    void fonction_callback(gpointer callback_data, gint callback_action, GtkWidget *widget);

    Dans ce cas, callback_data est la donnée supplémentaire envoyée à la fonction callback, callback_action et identique à la valeur donnée à l'élément et widget est l'élément lui-même.

    Le cinquième paramètre, item_type, définit de quoi est constitué l'élément du menu. Ce paramètre doit être une des valeurs suivantes :

    Valeur | Type d'élément
"<Branch>" | Elément ouvrant un autre menu.
NULL, "", "<Item>" | Elément classique avec du texte.
"<StockItem>" | Elément de type GtkStockItem.
"<ImageItem>" | Elément de type GtkImageMenuItem.
"<CheckItem>" | Elément de type GtkCheckMenuItem.
"<ToggleItem>" | Elément de type GtkToggleBtn.
"<RadioItem>" | Elément de type GtkRadioMenuItem.
"path" | Elément de type GtkRadioMenuItem. path correspond au path de l'élément principal du groupe de GtkRadioMenuItem.
"<Title>" | Affiche seulement le texte (grisé) de l'élément. Ce dernier ne peut être sélectionné.
"<Separator>" | Une ligne de séparation.
"<Tearoff>" | Elément pour détacher une partie du menu
"<LastBranch>" | Elément ouvrant un autre menu. Met l'élément à droite de la barre de menu.

    La seule subtilité ici, est pour les éléments de type GtkRadioMenuItem. Le premier bouton radio du groupe (ex : path = « /Choix/Pour ») aura item_type égal à « <RadioItem> » alors que les autres auront item_type égal à « /Choix/Pour ».

    Enfin pour terminer, le dernier paramètre extra_data devra être défini dans deux cas :

    • L'élément est de type « <StockItem> », alors extra_data sera l'identifiant du GtkStockItem à afficher dans le menu ;
    • L'élément est de type « <ImageItem> », alors extra_data sera un pointeur sur le GdkPixmap (pas encore étudié à ce stade du cours) à afficher.

    Nous savons donc maintenant comment déclarer le menu et il ne reste plus qu'à le créer.

    XIV-C-3. Création du menu

    La première chose à faire est de créer un objet pour récupérer les raccourcis claviers du menu. Pour cela nous allons utiliser l'objet GtkAccelGroup dont nous n'allons pas parler ici. La création de cet objet se fait à l'aide de cette fonction :

    GtkAccelGroup* gtk_accel_group_new(void);

    Ensuite il faut créer l'objet GtkItemFactory avec cette fonction :

    GtkItemFactory* gtk_item_factory_new(GType container_type, const gchar *path, GtkAccelGroup *accel_group);

    Le premier paramètre container_type , définit le type de menu que nous voulons créer. Il existe trois valeurs possibles :

    • GTK_TYPE_MENU_BAR, qui créera une barre de menu tout ce qu'il y a de plus normal ;
    • GTK_TYPE_MENU, qui créera un menu qui servira pour faire un menu popup ;
    • GTK_TYPE_OPTION_MENU, qui créera une sorte de bouton qui ouvrira le menu une fois cliqué.

    Le paramètre path est le nom que nous donnons au menu. Il représente la racine du menu et doit avoir la syntaxe suivante « <nom_du_menu> ».

    Le dernier paramètre permet de récupérer les raccourcis claviers dans le GtkAccelGroup. Ce paramètre peut être égal à NULL.

    Une fois le menu créer, il faut y insérer tous ses éléments avec la fonction :

    void gtk_item_factory_create_items(GtkItemFactory *ifactory, guint n_entries, GtkItemFactoryEntry *entries, gpointer callback_data);

    Voici le détail des différents paramètres :

    • ifactory est le GtkItemFactory que nous venons de créer ;
    • n_entries est le nombre d'éléments que nous allons ajouter ;
    • entries est le tableau de GtkItemFactoryEntry qui a été crée au début ;
    • callback_data est la donnée supplémentaire qui sera envoyé aux différentes fonctions callback. Cette donnée est unique et sera envoyée pour tous les éléments de entries qui ont le paramètre callback_action différents de zéro.

    Il faut ensuite récupérer de tout cela un GtkWidget pour pouvoir l'ajouter à notre fenêtre. Pour cela, il faut utiliser cette fonction :

    GtkWidget* gtk_item_factory_get_widget(GtkItemFactory *ifactory, const gchar *path);

    Les paramètres sont respectivement le GtkItemFactory créé précédemment ( ifactory ) avec le nom qui lui est associé (path).

    Et pour terminer tout cela, il faut associer les raccourcis à la fenêtre contenant le menu. Cela est possible par le biais d'une fonction du widget GtkWindow :

    void gtk_window_add_accel_group(GtkWindow *window, GtkAccelGroup *accel);

    Et voila, le menu est terminé.

    XIV-C-4. 3.4 Exemple.

    Nous allons reprendre l'exemple de la première partie, mais cette en utilisant des éléments de menu de type « <StockItem> ». En plus de cela, nous rajouterons en bas de la fenêtre un menu de type GTK_TYPE_OPTION_MENU.

    XIV-C-5. 3.5 Programme exemple.

    #include <stdlib.h>
#include <gtk/gtk.h>
void OnQuitter(gpointer data, guint callback_action,GtkWidget *widget);
void OnAbout(gpointer data, guint callback_action,GtkWidget *widget);
/* Définition des éléments du menu */
static GtkItemFactoryEntry MenuItem[] = {
    { "/_Fichier", NULL, NULL, 0, "<Branch>" },
    { "/Fichier/_Nouveau", NULL, NULL, 0, "<StockItem>", GTK_STOCK_NEW },
    { "/Fichier/_Ouvrir", NULL, NULL, 0, "<StockItem>", GTK_STOCK_OPEN },
    { "/Fichier/Enregi_strer", "<ctrl>S", NULL, 0, "<StockItem>", GTK_STOCK_SAVE },
    { "/Fichier/_Fermer", "<ctrl>F", NULL, 0, "<StockItem>", GTK_STOCK_CLOSE },
    { "/Fichier/Sep1", NULL, NULL, 0, "<Separator>" },
    { "/Fichier/_Quitter", NULL, OnQuitter, 1, "<StockItem>", GTK_STOCK_QUIT},
    { "/_?", NULL, NULL, 0, "<Branch>" },
    { "/?/_A propos de...", "<CTRL>A", OnAbout, 1, "<StockItem>", GTK_STOCK_HELP}
};
/* Nombre d' éléments du menu */
static gint iNbMenuItem = sizeof(MenuItem) / sizeof(MenuItem[0]);
int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pMenuBar;
    GtkItemFactory *pItemFactory;
    GtkAccelGroup *pAccel;
    gtk_init(&argc, &argv);
    /* Création de la fenêtre */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "Les menus");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
    pVBox = gtk_vbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);
    /* Création de la table d'accélération */
    pAccel = gtk_accel_group_new();
    /* Création du menu */
    pItemFactory = gtk_item_factory_new(GTK_TYPE_MENU_BAR, "<main>", pAccel);
    /* Récupération des éléments du menu */
    gtk_item_factory_create_items(pItemFactory, iNbMenuItem, MenuItem, (GtkWidget*)pWindow);
    /* Récupération du widget pour l'affichage du menu */
    pMenuBar = gtk_item_factory_get_widget(pItemFactory, "<main>");
    /* Ajout du menu en haut de la fenêtre */
    gtk_box_pack_start(GTK_BOX(pVBox), pMenuBar, FALSE, FALSE, 0);
    /* Association des raccourcis avec la fenêtre */
    gtk_window_add_accel_group(GTK_WINDOW(pWindow), pAccel);
    /* Création d un second menu de type GTK_TYPE_OPTION_MENU */
    pItemFactory = gtk_item_factory_new(GTK_TYPE_OPTION_MENU, "<main>", NULL);
    /* Récupération des éléments du menu */
    gtk_item_factory_create_items(pItemFactory, iNbMenuItem, MenuItem, (GtkWidget*)pWindow);
    /* Récupération du widget pour l'affichage du menu */
    pMenuBar = gtk_item_factory_get_widget(pItemFactory, "<main>");
    /* Ajout du menu en bas de la fenetre */
    gtk_box_pack_end(GTK_BOX(pVBox), pMenuBar, FALSE, FALSE, 0);
    gtk_widget_show_all(pWindow);
    gtk_main();
    return EXIT_SUCCESS;
}
void OnQuitter(gpointer data, guint callback_action,GtkWidget *widget)
{
    GtkWidget *pQuestion;
    pQuestion = gtk_message_dialog_new(GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_QUESTION,
        GTK_BUTTONS_YES_NO,
        "Voulez vous vraiment\n"
        "quitter le programme?");
    switch(gtk_dialog_run(GTK_DIALOG(pQuestion)))
    {
    case GTK_RESPONSE_YES:
        gtk_main_quit();
        break;
    case GTK_RESPONSE_NONE:
    case GTK_RESPONSE_NO:
        gtk_widget_destroy(pQuestion);
        break;
    }
}
void OnAbout(gpointer data, guint callback_action,GtkWidget *widget)
{
    GtkWidget *pAbout;
    pAbout = gtk_message_dialog_new(GTK_WINDOW(data),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        "Cours GTK+ 2.0\n"
        "http://gtk.developpez.com");
    gtk_dialog_run(GTK_DIALOG(pAbout));
    gtk_widget_destroy(pAbout);
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XIV-D. En savoir plus

    XIV-D-1. Les signaux

    Prototypes fonctions callback :

    GtkMenuShell

    • activate-current

      • void user_function(GtkMenuShell *menushell, gboolean force_hide, gpointer user_data);

    • cancel

      • void user_function(GtkMenuShell *menushell, gpointer user_data);

    • cycle-focus

      • void user_function(GtkMenuShell *menushell, GtkDirectionType arg1, gpointer user_data);

    • deactivate

      • void user_function(GtkMenuShell *menushell, gpointer user_data);

    • move-current

      • void user_function(GtkMenuShell *menushell, GtkMenuDirectionType direction, gpointer user_data);

    • selection-done

      • void user_function(GtkMenuShell *menushell, gpointer user_data);

    GtkMenuItem

    • activate

      • void user_function(GtkMenuItem *menuitem, gpointer user_data);

    • activate-item

      • void user_function(GtkMenuItem *menuitem, gpointer user_data);

    • toggle-size-allocate

      • void user_function(GtkMenuItem *menuitem, gint arg1, gpointer user_data);

    • toggle-size-request

      • void user_function(GtkMenuItem *menuitem, gint arg1, gpointer user_data);

    XIV-D-2. Fonctions non documentées

    GtkMenuShell

    void gtk_menu_shell_insert(GtkMenuShell *menu_shell, GtkWidget *child, gint position);
void gtk_menu_shell_deactivate(GtkMenuShell *menu_shell);
void gtk_menu_shell_select_item(GtkMenuShell *menu_shell, GtkWidget *menu_item);
void gtk_menu_shell_deselect(GtkMenuShell *menu_shell);
void gtk_menu_shell_activate_item(GtkMenuShell *menu_shell, GtkWidget *menu_item, gboolean force_deactivate);

    GtkMenu

    void gtk_menu_reorder_child(GtkMenu *menu, GtkWidget *child, gint position);
void gtk_menu_popup(GtkMenu *menu, GtkWidget *parent_menu_shell, GtkWidget *parent_menu_item, GtkMenuPositionFunc func, gpointer data, guint button, guint32 activate_time);
void gtk_menu_set_accel_group(GtkMenu *menu, GtkAccelGroup *accel_group);
GtkAccelGroup* gtk_menu_get_accel_group(GtkMenu *menu);
void gtk_menu_set_accel_path(GtkMenu *menu, const gchar *accel_path);
void gtk_menu_set_title(GtkMenu *menu, const gchar *title);
G_CONST_RETURN gchar* gtk_menu_get_title(GtkMenu *menu);
GtkWidget* gtk_menu_get_active(GtkMenu *menu);
void gtk_menu_set_active(GtkMenu *menu, guint index);
void gtk_menu_set_tearoff_state(GtkMenu *menu, gboolean torn_off);
void gtk_menu_attach_to_widget(GtkMenu *menu, GtkWidget *attach_widget, GtkMenuDetachFunc detacher);
void gtk_menu_detach(GtkMenu *menu);
GtkWidget* gtk_menu_get_attach_widget(GtkMenu *menu);

    GtkMenuItem

    void gtk_menu_item_set_right_justified(GtkMenuItem *menu_item, gboolean right_justified);
void gtk_menu_item_set_accel_path(GtkMenuItem *menu_item, const gchar *accel_path);
void gtk_menu_item_remove_submenu(GtkMenuItem *menu_item);
void gtk_menu_item_select(GtkMenuItem *menu_item);
void gtk_menu_item_deselect(GtkMenuItem *menu_item);
void gtk_menu_item_activate(GtkMenuItem *menu_item);
void gtk_menu_item_toggle_size_request(GtkMenuItem *menu_item, gint *requisition);
void gtk_menu_item_toggle_size_allocate(GtkMenuItem *menu_item, gint allocation);
gboolean gtk_menu_item_get_right_justified(GtkMenuItem *menu_item);
GtkWidget* gtk_menu_item_get_submenu(GtkMenuItem *menu_item);


Cours GTK 2

Table des matières

  • XV. La barre d'outils
    • XV-A. Utiliser une GtkToolbar
      • XV-A-1. Création
      • XV-A-2. Insertion d'éléments
      • XV-A-3. Les espaces.
      • XV-A-4. Orientation de la barre d'outils.
      • XV-A-5. Les styles.
      • XV-A-6. La taille des icônes.
      • XV-A-7. 1.7 Exemple.
      • XV-A-8. 1.8 Programme exemple.
    • XV-B. En savoir plus
      • XV-B-1. Fonction documentées

    • XV. La barre d'outils

    La barre d'outils est très utile pour placer en dessous du menu les commandes les plus utiliser comme faire/défaire, nouveau, ouvrir, sauvegarde, etc. Un widget, GtkToolbar, indispensable pour une bonne application.

    Image non disponible

    XV-A. Utiliser une GtkToolbar

    XV-A-1. Création

    Comme pour tous les autres widgets, c'est très simple :

    GtkWidget* gtk_toolbar_new(void);

    XV-A-2. Insertion d'éléments

    GtkToolbar étant un container, après l'avoir créée, il va falloir ajouter des widget dedans. Cela se divise en deux catégories.

    • Les widgets habituels comme les GtkButton, GtkToggleButton, GtkRadio pour lesquels GtkToolbar fourni des fonctions qui s'occupent de la création de ces derniers ;
    • Les autres widgets que nous pouvons ajouter mais c'est à nous de fournir les widgets donc il faudra les créer auparavant.

    Voici les premières fonctions qui permettent d'ajouter un bouton avec du texte et (ou) un icône :

    GtkWidget* gtk_toolbar_append_item(GtkToolbar *toolbar, const char *text, const char *tooltip_text, const char *tooltip_private_text,
      GtkWidget *icon, GtkSignalFunc callback, gpointer user_data);
GtkWidget* gtk_toolbar_prepend_item(GtkToolbar *toolbar, const char *text, const char *tooltip_text, const char *tooltip_private_text,
      GtkWidget *icon, GtkSignalFunc callback, gpointer user_data);
GtkWidget* gtk_toolbar_insert_item(GtkToolbar *toolbar, const char *text, const char *tooltip_text,const char *tooltip_private_text,
      GtkWidget *icon, GtkSignalFunc callback, gpointer user_data, gint position);

    La première fonction ajoute un bouton à la suite des autres boutons, la seconde l'ajoute en première position et la dernière à une position spécifique.

    Pour chaque fonction, le premier paramètre est la barre d'outil dans laquelle on veut ajouter un élément. Il faut comme d'habitude utiliser une macro de conversion qui cette fois est GTK_TOOLBAR() .

    Ensuite, le paramètre text n'est autre que le label du bouton. Le troisième paramètre, tooltip_text , est une aide qui s'affichera lorsque l'utilisateur laisse le pointeur de sa souris quelques secondes sur le bouton. Le paramètre suivant, tooltip_private_text , n'est plus utilisé car la partie qui gère ce paramètre est obsolète. Il faut donc mettre ce paramètre à NULL.

    Le paramètre icon est là pour définir l'image qui sera associée au bouton.

    Les deux paramètres servent à connecter une fonction callback au clic sur le bouton. Le paramètre callback est en fait le nom de la fonction callback et user_data est la donnée supplémentaire à passer à la fonction callback.

    Et pour terminer, la fonction gtk_toolbar_insert_item possède un paramètre supplémentaire position qui détermine à quelle position le bouton doit être ajouté. Si cette valeur est soit tros grande, soit négative, cette fonction aura le même effect que gtk_toolbar_append_item .

    Ensuite il est possible de créer des éléments selon un type de widget prédéfini, comme GtkRadioButton, GtkToggleButton, GtkButton :

    GtkWidget* gtk_toolbar_append_element(GtkToolbar *toolbar, GtkToolbarChildType type, GtkWidget *widget, const char *text,
      const char *tooltip_text, const char *tooltip_private_text, GtkWidget *icon, GtkSignalFunc callback, gpointer user_data);
GtkWidget* gtk_toolbar_prepend_element(GtkToolbar *toolbar, GtkToolbarChildType type, GtkWidget *widget, const char *text,
      const char *tooltip_text, const char *tooltip_private_text, GtkWidget *icon, GtkSignalFunc callback, gpointer user_data);
GtkWidget* gtk_toolbar_insert_element(GtkToolbar *toolbar, GtkToolbarChildType type, GtkWidget *widget, const char *text,
      const char *tooltip_text, const char *tooltip_private_text, GtkWidget *icon, GtkSignalFunc callback, gpointer user_data, gint position);

    La majorité des paramètres de ces fonctions ont été détaillés précédemment, nous n'allons donc étudier que les nouveaux.

    Tout d'abord, le paramètre type permet de définir le type de widget que nous allons ajouter, et peut prendre une de ces valeurs :

    • GTK_TOOLBAR_CHILD_SPACE pour ajouter un espace ;
    • GTK_TOOLBAR_CHILD_BUTTON pour ajouter un GtkButton ;
    • GTK_TOOLBAR_CHILD_TOGGLEBUTTON pour ajouter un GtkToggleButton ;
    • GTK_TOOLBAR_CHILD_RADIOBUTTON pour ajouter un GtkRadioButton ;
    • GTK_TOOLBAR_CHILD_WIDGET pour ajouter un widget quelconque.

    Il y a ensuite le paramètre widget qui doit être utilisé dans deux cas. Le premier cas est bien entendu, si nous ajoutons un élément de type GTK_TOOLBAR_CHILD_WIDGET . Alors widget sera en fait le widget que nous voulons ajouter.

    Le deuxième cas est si nous ajoutons un élément de type GTK_TOOLBAR_CHILD_RADIOBUTTON. Nous avons vu que les GtkRadioButton fonctionnés par groupe, alors dans ce cas, pour grouper les boutons radios le paramètre widget doit être un GtkRadioButton ajouté précédemment pour que GTK+ puisse les grouper. Bien sûr, s'il s'agit du premier GtkRadioButton, il faut mettre widget à NULL.

    Dans tous les autres cas, le paramètre widget doit obligatoirement être à NULL.

    Et maintenant, voila les fonctions pour ajouter n'importe quel type de widget :

    void gtk_toolbar_append_widget(GtkToolbar *toolbar, GtkWidget *widget, const char *tooltip_text, const char *tooltip_private_text);
void gtk_toolbar_prepend_widget(GtkToolbar *toolbar, GtkWidget *widget, const char *tooltip_text, const char *tooltip_private_text);
void gtk_toolbar_insert_widget(GtkToolbar *toolbar, GtkWidget *widget, const char *tooltip_text, const char *tooltip_private_text, gint position);

    Cette fois, tout est simple, le paramètre widget est simplement le widget que nous voulons ajouter. Il faudra tout de même connecter manuellement les signaux du widget ajouté.

    Pour finir, il est possible d'utiliser les GtkStockItem pour créer un bouton. Voici la fonction qui permet cela :

    GtkWidget* gtk_toolbar_insert_stock(GtkToolbar *toolbar, const gchar *stock_id, const char *tooltip_text, const char *tooltip_private_text,
      GtkSignalFunc callback, gpointer user_data, gint position);

    Le paramètre stock_id est tout simplement l'identifiant du GtkStockItem qui figurera sur le bouton.

    XV-A-3. Les espaces.

    Nous avons déjà vu que les fonctions de type gtk_toolbar_*_element permettaient d'ajouter un espace pour rendre plus claire la barre d'outils. Il existe en plus de cela trois autres fonctions :

    void gtk_toolbar_append_space(GtkToolbar *toolbar );
void gtk_toolbar_prepend_space(GtkToolbar *toolbar );
void gtk_toolbar_insert_space(GtkToolbar *toolbar, guint pos );

    Comme d'habitude la première fonction ajoute un espace à la suite des autres éléments, la deuxième au tout début de la barre d'outils et la troisième à une position particulière.

    Pour supprimer un espace de la barre d'outils, il existe cette fonction :

    void gtk_toolbar_remove_space(GtkToolbar *toolbar, guint pos);

    XV-A-4. Orientation de la barre d'outils.

    La barre d'outil peut être orientée verticalement ou horizontalement et cela à n'importe quel moment a l'aide de cette fonction :

    void gtk_toolbar_set_orientation(GtkToolbar *toolbar, GtkOrientation orientation) ;

    Le paramètre orientation peut prendre deux valeurs :

    • GTK_ORIENTATION_HORIZONTAL ;
    • GTK_ORIENTATION_VERTICAL.

    Et, cette fonction permet de connaître l'orientation de la barre d'outil :

    GtkOrientation gtk_toolbar_get_oritentation(GtkToolbar *toolbar);

    XV-A-5. Les styles.

    Il est possible de changer la façon d'afficher certains éléments de la barre d'outils : afficher que le texte, seulement les icônes ou les deux. Voila la fonction qui permet de contrôler cela :

    void gtk_toolbar_set_style(GtkToolbar *toolbar, GtkToolbarStyle style) ;

    Le paramètre style peut prendre 4 valeurs différentes :

    • GTK_TOOLBAR_ICONS pour n'afficher que l'icône ;
    • GTK_TOOLBAR_TEXT pour n'afficher que le texte ;
    • GTK_TOOLBAR_BOTH pour afficher le texte en dessous de l'icône (valeur par défaut) ;
    • GTK_TOOLBAR_BOTH_HORIZ pour afficher le texte à côté de l'icône.

    Et pour finir, cette fonction permet de connaître le style de la barre d'outil :

    GtkToolbarStyle gtk_toolbar_get_style(GtkToolbar *toolbar);

    XV-A-6. La taille des icônes.

    Enfin pour terminer, GTK+ nous offre la possibilité de modifier la taille des icônes de la barre d'outils avec cette fonction :

    void gtk_toolbar_set_icon_size(GtkToolbar *toolbar, GtkIconSize icon_size);

    Le paramètre icon_size est du type GtkIconSize que nous avons déjà rencontré dans le chapitre sur les images. Nous allons tout de même rappeler les différentes valeurs possibles qui sont les suivantes :

    • GTK_ICON_SIZE_MENU ;
    • GTK_ICON_SIZE_SMALL_TOOLBAR ;
    • GTK_ICON_SIZE_LARGE_TOOLBAR (valeur par défaut) ;
    • GTK_ICON_SIZE_BUTTON ;
    • GTK_ICON_SIZE_DND ;
    • GTK_ICON_SIZE_DIALOG.

    Et bien sûr, pour connaître la taille des icônes, nous avons la fonction :

    GtkIconSize gtk_toolbar_get_icon_size(GtkToolbar *toolbar);

    XV-A-7. 1.7 Exemple.

    Dans cet exemple, nous allons juste créer une barre d'outils dans laquelle nous allons ajouter quelques boutons dont deux d'entre eux permettront de changer l'orientation de la barre d'outil.

    XV-A-8. 1.8 Programme exemple.

    #include <stdlib.h>
#include <gtk/gtk.h>

void OnChangeOrientation(GtkWidget *widget, gpointer data);

static GtkWidget *pToolbar = NULL;

int main(int argc, char **argv)
{
   GtkWidget *pWindow;
   GtkWidget *pVBox;

   gtk_init(&argc, &argv);

   /* Création de la fenêtre */
   pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_window_set_title(GTK_WINDOW(pWindow), "GtkToolbar");
   gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
   g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

   pVBox = gtk_vbox_new(FALSE, 0);
   gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

   /* Création de la barre d'outils */
   pToolbar = gtk_toolbar_new();
   gtk_box_pack_start(GTK_BOX(pVBox), pToolbar, FALSE, FALSE, 0);

   /* Création à partir de stock */
   gtk_toolbar_insert_stock(GTK_TOOLBAR(pToolbar),
      GTK_STOCK_NEW,
      "Nouveau",
      NULL,
      NULL,
      NULL,
      -1);
   gtk_toolbar_insert_stock(GTK_TOOLBAR(pToolbar),
      GTK_STOCK_OPEN,
      "Ouvrir",
      NULL,
      NULL,
      NULL,
      -1);
   gtk_toolbar_insert_stock(GTK_TOOLBAR(pToolbar),
      GTK_STOCK_SAVE,
      "Enregistrer",
      NULL,
      NULL,
      NULL,
      -1);
   gtk_toolbar_insert_stock(GTK_TOOLBAR(pToolbar),
      GTK_STOCK_QUIT,
      "Fermer",
      NULL,
      G_CALLBACK(gtk_main_quit),
      NULL,
      -1);

   /* Insertion d'un espace */
   gtk_toolbar_append_space(GTK_TOOLBAR(pToolbar));

   /* Création a partir de stock */
   gtk_toolbar_insert_stock(GTK_TOOLBAR(pToolbar),
      GTK_STOCK_GO_FORWARD,
      "Horizontale",
      NULL,
      G_CALLBACK(OnChangeOrientation),
      GINT_TO_POINTER(GTK_ORIENTATION_HORIZONTAL),
      -1);
   gtk_toolbar_insert_stock(GTK_TOOLBAR(pToolbar),
      GTK_STOCK_GO_DOWN,
      "Verticale",
      NULL,
      G_CALLBACK(OnChangeOrientation),
      GINT_TO_POINTER(GTK_ORIENTATION_VERTICAL),
      -1);

   /* Modification de la taille des icônes */
   gtk_toolbar_set_icon_size(GTK_TOOLBAR(pToolbar),
      GTK_ICON_SIZE_BUTTON);
   /* Affichage uniquement des icônes */
   gtk_toolbar_set_style(GTK_TOOLBAR(pToolbar),
      GTK_TOOLBAR_ICONS);

   gtk_widget_show_all(pWindow);

   gtk_main();

   return EXIT_SUCCESS;
}

void OnChangeOrientation(GtkWidget *widget, gpointer data)
{
   /* Modification de l'orientation */
   gtk_toolbar_set_orientation(GTK_TOOLBAR(pToolbar),
      GPOINTER_TO_INT(data));
}

    Résultat :

    Image non disponible
    Image non disponible

    XV-B. En savoir plus

    XV-B-1. Fonction documentées

    void gtk_toolbar_set_tooltips(GtkToolbar *toolbar, gboolean enable);

    Permet de définir s'il faut afficher l'aide.

    Entrée(s) :

    • toolbar : la barre d'outils
    • enable : TRUE pour afficher l'aide, FALSE sinon.

    Sortie : rien

    gboolean gtk_toolbar_get_tooltips(GtkToolbar *toolbar);

    Pour savoir si le programme affiche l'aide.

    Entrée(s) :

    • toolbar : la barre d'outils

    Sortie : TRUE si l'aide est affichable, FALSE sinon.

    void gtk_toolbar_unset_icon_size(GtkToolbar *toolbar);

    Change la taille des icônes en la remettant à la valeur par défaut.

    Entrée(s) :

    • toolbar : la barre d'outils

    Sortie : rien

    void gtk_toolbar_unset_style(GtkToolbar *toolbar);

    Change le style de la barre d'outils en le remettant à la valeur par défaut.

    Entrée(s) :

    • toolbar : la barre d'outils

    Sortie : rien


Cours GTK 2

Table des matières

  • XVI. La barre d'état
    • XVI-A. Utilisation d'une GtkStatusBar
      • XVI-A-1. Création
      • XVI-A-2. Utilisation
      • XVI-A-3. 1.3 Exemple
      • XVI-A-4. 1.4 Programme exemple
    • XVI-B. En savoir plus
      • XVI-B-1. Signaux
      • XVI-B-2. Fonctions documentées

    • XVI. La barre d'état

    La barre d'état peut avoir plusieurs utilités. Tout d'abord lorsqu'un programme est en train d'effectuer une action, elle peut servir à signaler à l'utilisateur ce que le programme fait. Elle peut aussi servir à expliquer l'utilité d'un bouton (ou d'un élément du menu). Nous allons étudier en détail le fonctionnement de ce widget nommé GtkStatusBar.

    Image non disponible

    XVI-A. Utilisation d'une GtkStatusBar

    XVI-A-1. Création

    La création de ce widget est aussi simple que pour tous les autres widgets. Il suffit d'utiliser cette fonction :

    GtkWidget *gtk_statusbar_new(void);

    XVI-A-2. Utilisation

    Regardons tout d'abord comment fonctionne une GtkStatusBar. Les messages sont mis dans une pile, l'élément le plus haut de la pile étant affiché. Pour gérer cette pile, Gtk+ offre trois fonctions différentes.

    Mais avant de pouvoir gérer cette pile, il faut connaître l'identifiant de la partie du programme qui ajoute un message dans la pile :

    guint gtk_statusbar_get_context_id(GtkStatusbar *statusbar, const gchar *context_description);

    Cette fonction va automatiquement ajouter un contexte, et renvoyer la valeur correspondante. Par la suite cet identifiant est à utiliser pour ajouter ou enlever des éléments à la pile.

    Pour ajouter un élément, il n'y a qu'une seule fonction :

    guint gtk_statusbar_push(GtkStatusbar *statusbar, guint context_id, const gchar *text);
    • statusbar : pointeur sur la barre d'état précédemment créée.
    • ontext_id : identifiant origine message.
    • text : message à afficher.

    Après l'appel de cette fonction, le message est automatiquement ajouté en haut de la pile et est affiché dans la barre d'état. La valeur de retour correspond à l'identifiant du message dans la pile qui peut être utile pour sa suppression.

    Pour supprimer un message de la pile, nous avons cette fois deux fonctions différentes :

    void gtk_statusbar_pop(GtkStatusbar *statusbar, guint context_id);
void gtk_statusbar_remove(GtkStatusbar *statusbar, guint context_id, guint message_id);

    La première fonction enlève l'élément le plus haut placé dans la pile provenant de la partie du programme ayant l'identifiant context_id. C'est là que context_id prend toute son importance.

    La deuxième fonction enlève l'élément par son identifiant et celui de son contexte.

    Nous avons maintenant tout ce qu'il faut pour utiliser une barre d'état dans notre application.

    XVI-A-3. 1.3 Exemple

    Nous allons construire une application comportant deux boutons et barre d'état. Le but de cette application et d'afficher un message dans la barre d'état lorsque la souris survole un des boutons.

    Nous allons d'abord créer notre barre d'état :

    StatusBar = gtk_status_bar_new();

    Ensuite nous allons l'insérer dans la fenêtre principale. En général, la barre d'état se trouve en bas d'une fenêtre, nous allons donc utiliser la fonction gtk_box_pack_end :

    gtk_box_pack_end(GTK_BOX(VBox),StatusBar,FALSE,FALSE,0);

    Nous allons créer deux contextes différents (un pour chaque bouton) pour l'utilisation de la barre d'état :

    ContextId1 = gtk_statusbar_get_context_id(GTK_STATUSBAR(StatusBar), "ExitMsg");
ContextId2 = gtk_statusbar_get_context_id(GTK_STATUSBAR(StatusBar), "AboutMsg");

    Il ne reste plus qu'à connecter les signaux « enter » et « leave » de deux boutons et écrire les fonctions callback. Dans la fonction de connexion, le ContextIdx sera passé en paramètre data, mais il faudra utiliser la macro de conversion GINT_TO_POINTER().

    XVI-A-4. 1.4 Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

static GtkWidget *pStatusBar;

void OnAboutBtn(GtkWidget *pButton, GtkWidget *pWindow);
void OnExitBtnEnter(GtkWidget *pButton, gpointer iContextId);
void OnAboutBtnEnter(GtkWidget *pButton, gpointer iContextId);
void ClearStatus(GtkWidget *pButton, gpointer iContextId);

int main(int argc, char **argv)
{
    GtkWidget* pWindow;
    GtkWidget* pVBox;
    GtkWidget* pExitButton;
    GtkWidget *pAboutButton;
    guint iContextId1;
    guint iContextId2;

    gtk_init(&argc, &argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkStatusbar");
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox=gtk_vbox_new(FALSE,5);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    pExitButton=gtk_button_new_with_label("Quitter");
    gtk_box_pack_start(GTK_BOX(pVBox), pExitButton, TRUE, FALSE, 5);
    g_signal_connect(G_OBJECT(pExitButton), "clicked", G_CALLBACK(gtk_main_quit), NULL);

    pAboutButton=gtk_button_new_with_label("A propos...");
    gtk_box_pack_start(GTK_BOX(pVBox), pAboutButton, TRUE, FALSE, 5);
    g_signal_connect(G_OBJECT(pAboutButton), "clicked", G_CALLBACK(OnAboutBtn), pWindow);

    /* Création de la barre d'état */
    pStatusBar = gtk_statusbar_new();

    gtk_box_pack_end(GTK_BOX(pVBox), pStatusBar, FALSE, FALSE, 0);

    /* Création des contextes */
    iContextId1 = gtk_statusbar_get_context_id(GTK_STATUSBAR(pStatusBar), "ExitMsg");
    iContextId2 = gtk_statusbar_get_context_id(GTK_STATUSBAR(pStatusBar), "AboutMsg");

    g_signal_connect(G_OBJECT(pExitButton), "enter", G_CALLBACK(OnExitBtnEnter),
        GINT_TO_POINTER(iContextId1));
    g_signal_connect(G_OBJECT(pAboutButton), "enter", G_CALLBACK(OnAboutBtnEnter),
        GINT_TO_POINTER(iContextId2));
    g_signal_connect(G_OBJECT(pExitButton), "leave", G_CALLBACK(ClearStatus),
        GINT_TO_POINTER(iContextId1));
    g_signal_connect(G_OBJECT(pAboutButton), "leave", G_CALLBACK(ClearStatus),
        GINT_TO_POINTER(iContextId2));

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

void OnAboutBtn(GtkWidget *pButton, GtkWidget *pWindow)
{
    GtkWidget *pAboutDlg;

    pAboutDlg = gtk_message_dialog_new(GTK_WINDOW(pWindow),
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        "Cours GTK+ 2.0\n"
        "GtkStatusbar\n"
        "http://gtk.developpez.com");

    gtk_dialog_run(GTK_DIALOG(pAboutDlg));

    gtk_widget_destroy(pAboutDlg);
}

void OnExitBtnEnter(GtkWidget *pButton, gpointer iContextId)
{
    /* Ajout d'un message */
    gtk_statusbar_push(GTK_STATUSBAR(pStatusBar), GPOINTER_TO_INT(iContextId), "Quitter l'application");
}

void OnAboutBtnEnter(GtkWidget *pButton, gpointer iContextId)
{
    /* Ajout d'un message */
    gtk_statusbar_push(GTK_STATUSBAR(pStatusBar), GPOINTER_TO_INT(iContextId), "Informations");
}

void ClearStatus(GtkWidget *pButton, gpointer iContextId)
{
    /* Suppression d'un message */
    gtk_statusbar_pop(GTK_STATUSBAR(pStatusBar), GPOINTER_TO_INT(iContextId));
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XVI-B. En savoir plus

    XVI-B-1. Signaux

    Prototypes fonctions callback :

    • text-popped

      • Ce signal est émis par la barre d'état lorsqu'un message est supprimé.

        void user_function(GtkStatusbar *statusbar, guint context_id, gchar *text, gpointer user_data);

    • text-pushed

      • Ce signal est émis par la barre d'état lorsqu'un message est ajouté.

        void user_function(GtkStatusbar *statusbar, guint context_id, gchar *text, gpointer user_data);

    XVI-B-2. Fonctions documentées

    void gtk_statusbar_set_has_resize_grip(GtkStatusbar *statusbar, gboolean setting);

    Définit si la zone à droite de la barre permet de redimensionner la fenêtre conteneur.

    Entrée(s) :

    • statusbar : la barre d'état.
    • setting : TRUE pour oui, FALSE sinon.

    Sortie : rien.

    gboolean gtk_statusbar_get_has_resize_grip(GtkStatusbar *statusbar);

    Pour savoir si la barre d'état permet de redimensionner la fenêtre conteneur.

    Entrée(s) :

    • statusbar : la barre d'état.

    Sortie : TRUE si oui, FALSE sinon.


Cours GTK 2

Table des matières

  • XVII. La sélection de valeurs numériques
    • XVII-A. Introduction
      • XVII-A-1. Structure de l'objet GtkAdjustment
      • XVII-A-2. Les fonctions de bases
    • XVII-B. Le widget GtkScrollbar
      • XVII-B-1. Création d'une GtkScrollbar
      • XVII-B-2. La gestion des valeurs.
      • XVII-B-3. Exemple
      • XVII-B-4. Programme exemple
    • XVII-C. Le widget GtkScale
      • XVII-C-1. Création
      • XVII-C-2. La gestion des valeurs.
      • XVII-C-3. Affichage de la valeur sous forme de label.
      • 3.4 Exemple.
      • XVII-C-5. 3.5 Programme exemple.
    • XVII-D. Le widget GtkSpinButton
      • XVII-D-1. Création
      • XVII-D-2. Gestion des valeurs
      • XVII-D-3. Exemple
      • XVII-D-4. Programme exemple
    • XVII-E. En savoir plus
      • XVII-E-1. Signaux
      • XVII-E-2. Fonction non documentées

    • XVII. La sélection de valeurs numériques

    Nous allons dans ce chapitre étudier trois widgets différents qui vont nous permettre de définir une valeur numérique sans avoir à la saisir au clavier. Le premier widget que nous allons étudier est GtkScrollbar qui se dérive en deux widgets différents, un qui est vertical et l'autre horizontal. Puis nous étudierons le widget GtkScale qui lui aussi se décline en un widget vertical et un horizontal. Et nous terminerons avec le widget GtkSpinButton.

    Image non disponible

    XVII-A. Introduction

    Ces trois widgets permettent de choisir une valeur numérique à l'intérieur d'une fourchette de valeurs bien définies. Le fonctionnement similaire de ces trois widgets est rendu possible par l'utilisation de l'objet GtkAdjustment. Nous allons donc tout d'abord acquérir les notions nécessaires sur ce widget pour comprendre les widgets de sélection.

    XVII-A-1. Structure de l'objet GtkAdjustment

    struct _GtkAdjustment
{
    GtkObject parent_instance;
    gdouble lower;
    gdouble upper;
    gdouble value;
    gdouble step_increment;
    gdouble page_increment;
    gdouble page_size;
}

    La propriété parent_instance nous dit tout simplement que l'objet GtkAdjustment dérive directement de GtkObject. Les valeurs lower et upper déterminent respectivement la valeur minimale et la valeur maximale que peut prendre le widget qui utilise cet objet. La valeur value donne la valeur actuelle telle qu'elle est définie par le widget. Les valeurs step_increment et page_increment définissent de combien value sera modifiée à chaque fois que l'utilisateur demande d'augmenter ou de diminuer la valeur. Nous verrons la différence entre ces deux valeurs avec l'étude des widgets GtkScrollbar et GtkScale. La dernière propriété page_size définit la taille d'une page, mais là aussi cela deviendra plus clair avec l'étude des widgets qui va suivre.

    XVII-A-2. Les fonctions de bases

    La première fonction de base est bien sûr la fonction qui permet de créer un tel objet :

    GtkObject* gtk_adjustment_new(gdouble value, gdouble lower, gdouble upper, gdouble step_increment, gdouble page_increment, gdouble page_size);

    Nous retrouvons ici tous les paramètres définis précédemment.

    Ensuite pour modifier ou récupérer la valeur actuelle de l'objet, il y a ces deux fonctions :

    void gtk_adjustment_set_value(GtkAdjustment *adjustment, gdouble value);
gdouble gtk_adjustment_get_value(GtkAdjustment *adjustment);

    Bien entendu la première fonction change le widget utilisateur pour que sa position représente la valeur value, et la deuxième fonction récupère cette valeur.

    Et pour terminer voici la fonction qui permet de modifier les valeurs limites de l'objet :

    void gtk_adjustment_clamp_page(GtkAdjustment *adjustment, gdouble lower, gdouble upper);

    Nous avons vu l'essentiel des fonctions de l'objet GtkAdjustment, mais en règle générale, il n'est pas nécessaire de les utiliser, car les widgets qui nécessitent cet objet, ont leurs propres fonctions d'accès et de modification des paramètres.

    XVII-B. Le widget GtkScrollbar

    Comme nous l'avons dit dans la présentation, ce widget se décline en deux variantes différentes :

    • Une horizontale avec le widget GtkHScrollbar ;
    • Une verticale avec le widget GtkVScrollbar.

    Mais nous allons voir que mise à part les fonctions de création, toutes les autres opérations se font avec les mêmes fonctions quel que soit le widget utilisé.

    XVII-B-1. Création d'une GtkScrollbar

    Voici donc les deux fonctions de création disponibles :

    GtkWidget* gtk_vscrollbar_new(GtkAdjustment *adjustment);
GtkWidget* gtk_hscrollbar_new(GtkAdjustment *adjustment);

    La première fonction crée donc une GtkVScrollbar et la seconde une GtkHScrollbar. De plus, nous voyons qu'il faut obligatoirement un GtkAdjustment pour définir les paramètres du widget. Il va donc falloir dans un premier temps créer un GtkAdjustment et définir ses valeurs.

    Pour ce qui est des valeurs lower , upper , et value , il n'y a pas de problème. Par contre pour les valeurs step_increment et page_increment , il faut connaître un peu le fonctionnement de ce widget. Regardons d'abord à quoi ressemble un widget GtkHScrollbar.

    Image non disponible

    La zone 1 est en fait l'afficheur de la valeur actuelle prise par la GtkScrollbar. Elle permet aussi de sélectionner une valeur en le déplaçant à l'aide de la souris. Les deux zones 2 permettent de modifier la valeur de la zone 1. Un clic de souris sur une de ces zones modifiera la valeur de plus ou moins la valeur de step_increment . Les zones 3 ont le même objectif que les zones 2 mais cette fois avec une modification de valeur de page_increment .

    Il reste encore le dernier paramètre à configurer. Ce dernier, page_size , détermine la taille de la zone 1. Si, par exemple le widget GtkScrollbar a une valeur minimale de 0 et une valeur maximale de 100, et nous définissons page_size comme ayant une valeur de 50, la zone 1 du widget aura une longueur égale à la moitié de celle du widget. Le dernier point sur ce paramètre est qu'il détermine aussi la valeur qu'il faut donner à upper . Ce widget fonctionnant en utilisant des pages, la valeur upper n'est jamais atteinte. Si l'on veut pouvoir choisir une valeur entre 0 et 100 avec un page_size égale à 50, il faudra mettre upper à 150 soit l' upper théorique plus page_size .

    En effet, ce widget n'a pas comme vocation première de permettre de sélectionner des valeurs numériques mais plutôt l'affichage d'autre widget dont la hauteur (ou la largeur) est supérieure à la zone d'affichage. Ainsi lorsque la barre de sélection est complètement à gauche, la valeur de la GtkScrollbar est bien de 0, mais elle considère qu'elle affiche une zone allant de 0 à page_size (50 dans notre exemple). De ce fait, si la barre de sélection est complètement à droite la valeur de la GtkScrollbar sera égale à upper-page_size et considérera qu'elle affiche une zone allant d' upper-page_size à upper (50 à 100 dans notre exemple). Pour conclure, il faut toujours donner à upper la valeur maximale que nous souhaitons pouvoir sélectionner plus la valeur de page_size .

    Nous savons maintenant comment configurer le GtkAdjustment afin de créer proprement une GtkScrollbar.

    XVII-B-2. La gestion des valeurs.

    Nous allons maintenant traiter les fonctions de gestion d'une GtkScrollbar. Pour cela, nous n'allons pas utiliser les widgets GtkHScrollbar et GtkVScrollbar car il n'y a pour chaque widget qu'une seule fonction de création, ni le widget GtkScrollbar car il ne possède aucune fonction, mais le widget GtkRange dont dérive toute cette panoplie.

    Tout d'abord pour fixer la valeur d'une GtkScrollbar, nous avons à notre disposition la fonction suivante :

    void gtk_range_set_value(GtkRange *range, gdouble value);

    Bien sûr le premier paramètre est la GtkScrollbar dont nous voulons fixer la valeur. Il faut pour cela utiliser la macro de conversion GTK_RANGE() . Le deuxième paramètre est la valeur que nous voulons donner à notre GtkScrollbar.

    A l'inverse, la fonction nous permettant de connaître la valeur de la GtkScrollbar est :

    gdouble gtk_range_get_value(GtkRange *range);

    Ensuite le widget GtkRange nous offre la possibilité de modifier les bornes du widget ainsi que les différents pas avec ces deux fonctions :

    void gtk_range_set_increments(GtkRange *range, gdouble step, gdouble page);
void gtk_range_set_range(GtkRange *range, gdouble min, gdouble max);

    La première fonction modifie les pas de modification. Les paramètres step et page correspondent aux paramètres step_increment et page_increment du GtkAdjustment associé au widget.

    La deuxième fonction permet de modifié les valeurs minimale et maximale que le widget peut prendre. Là encore, il faut penser au problème de la valeur maximale pour bien configurer max .

    Une autre possibilité pour modifier ces paramètres est de créer un nouveau GtkAdjustment et de l'associer à la GtkScrollbar avec cette fonction :

    void gtk_range_set_adjustment(GtkRange *range, GtkAdjustment *adjustment);

    XVII-B-3. Exemple

    Notre premier exemple de ce chapitre va nous permettre de sélection les valeurs RGB que nous souhaitons affecter. La couleur formée par ces valeurs ne sera pas affichée car nous nous contenterons d'afficher ces valeurs dans un label.

    Pour modifier les valeurs des labels en fonction des modifications des GtkScrollbar, nous allons capturer le signal « value-changed » de chaque GtkScrollbar.

    XVII-B-4. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>
void OnScrollbarChange(GtkWidget *pWidget, gpointer data);
int main(int argc,char **argv)
{
   GtkWidget* pWindow;
   GtkWidget *pMainVBox;
   GtkWidget *pFrame;
   GtkWidget *pColorBox;
   GtkWidget *pLabel;
   GtkWidget *pScrollbar;
   GtkObject *Adjust;
   gtk_init(&argc,&argv);
   pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_window_set_title(GTK_WINDOW(pWindow), "GtkScrollbar");
   gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
   gtk_container_set_border_width(GTK_CONTAINER(pWindow), 4);
   pMainVBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pWindow), pMainVBox);
   pFrame = gtk_frame_new("Rouge");
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   pColorBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pFrame), pColorBox);
   /* Label d'affichage de valeur R*/
   pLabel = gtk_label_new("0");
   gtk_box_pack_start(GTK_BOX(pColorBox), pLabel, FALSE, FALSE, 0);
   /* Création d un GtkAdjustment */
   Adjust = gtk_adjustment_new(0, 0, 256, 1, 10, 1);
   /* Creation d une scrollbar horizontale*/
   pScrollbar = gtk_hscrollbar_new(GTK_ADJUSTMENT(Adjust));
   gtk_box_pack_start(GTK_BOX(pColorBox), pScrollbar, TRUE, TRUE, 0);
   /* Connexion du signal pour modification de l'affichage */
   g_signal_connect(G_OBJECT(pScrollbar), "value-changed",
      G_CALLBACK(OnScrollbarChange), (GtkWidget*)pLabel);
   /* Idem pour G */
   pFrame = gtk_frame_new("Vert");
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   pColorBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pFrame), pColorBox);
   pLabel = gtk_label_new("0");
   gtk_box_pack_start(GTK_BOX(pColorBox), pLabel, FALSE, FALSE, 0);
   Adjust = gtk_adjustment_new(0, 0, 256, 1, 10, 1);
   pScrollbar = gtk_hscrollbar_new(GTK_ADJUSTMENT(Adjust));
   gtk_box_pack_start(GTK_BOX(pColorBox), pScrollbar, TRUE, TRUE, 0);
   g_signal_connect(G_OBJECT(pScrollbar), "value-changed",
      G_CALLBACK(OnScrollbarChange), (GtkWidget*)pLabel);
   /* Idem pour B */
   pFrame = gtk_frame_new("Bleu");
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   pColorBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pFrame), pColorBox);
   pLabel = gtk_label_new("0");
   gtk_box_pack_start(GTK_BOX(pColorBox), pLabel, FALSE, FALSE, 0);
   Adjust = gtk_adjustment_new(0, 0, 256, 1, 10, 1);
   pScrollbar = gtk_hscrollbar_new(GTK_ADJUSTMENT(Adjust));
   gtk_box_pack_start(GTK_BOX(pColorBox), pScrollbar, TRUE, TRUE, 0);
   g_signal_connect(G_OBJECT(pScrollbar), "value-changed",
      G_CALLBACK(OnScrollbarChange), (GtkWidget*)pLabel);
   gtk_widget_show_all(pWindow);
   g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
   gtk_main();
   return EXIT_SUCCESS;
}
void OnScrollbarChange(GtkWidget *pWidget, gpointer data)
{
   gchar* sLabel;
   gint iValue;
   /* Récupération de la valeur de la scrollbar */
   iValue = gtk_range_get_value(GTK_RANGE(pWidget));
   /* Création du nouveau label */
   sLabel = g_strdup_printf("%d", iValue);
   /* Modification du label */
   gtk_label_set_text(GTK_LABEL(data), sLabel);
   /* Liberation memoire */
   g_free(sLabel);
}

    Résultat :

    Image non disponible

    XVII-C. Le widget GtkScale

    Les widgets GtkHScale et GtkVScale ont un fonctionnement quasi identique aux widget GtkHScrollbar et GtkVScrollbar mais sont plus simple d'utilisation. Un des avantages de ce widget est qu'il offre la possibilité d'afficher tout seul sa valeur.

    XVII-C-1. Création

    Comme pour les GtkScrollbar, nous pouvons créer des GtkScale à l'aide de GtkAdjustment avec ces fonctions :

    GtkWidget* gtk_vscale_new(GtkAdjustment *adjustment);
GtkWidget* gtk_hscale_new(GtkAdjustment *adjustment);

    La première fonction crée donc un GtkScale vertical alors la deuxième un GtkScale horizontal.

    Mais pour ne pas avoir à créer un objet GtkAdjustment, les créateurs de GTK+ ont pensé à nous fournir une fonction qui n'en a pas besoin :

    GtkWidget* gtk_vscale_new_with_range(gdouble min, gdouble max, gdouble step);
GtkWidget* gtk_hscale_new_with_range(gdouble min, gdouble max, gdouble step);

    Avec ces quatre fonctions, la signification de step (ou step_increment si l'on utilise un GtkAdjustment) est un peu différente car le widget GtkScale n'a pas de bouton fléché comme GtkScrollbar. Cette valeur est utilisée pour un déplacement à l'aide des touches fléchées lorsque le widget a le focus. De plus, si nous n'utilisons pas de GtkAdjustment, la valeur de page_increment est égale à dix fois celle de step.

    XVII-C-2. La gestion des valeurs.

    Pour récupérer ou fixer les différentes valeurs d'un widget GtkScale il faut, comme pour le widget GtkScrollbar, utiliser les fonctions du widget GtkRange qui ont été décrites plus haut. Nous n'allons donc pas revenir dessus.

    XVII-C-3. Affichage de la valeur sous forme de label.

    Comme nous l'avons dit dans la présentation de ce widget, il s'occupe seul d'afficher et de mettre à jour un label qui affiche la valeur du widget. Par défaut, le label est affiché au-dessus du GtkScale. Etudions les quelques fonctions qui nous sont fournis pour gérer cet affichage.

    void gtk_scale_set_draw_value(GtkScale *scale, gboolean draw_value);
gboolean gtk_scale_get_draw_value(GtkScale *scale);

    La première fonction nous permet de décider si nous voulons qu'un label s'affiche ou pas. Pour cela il suffit de mettre le paramètre draw_value à TRUE si nous souhaitons qu'il s'affiche ou FALSE si nous ne le souhaitons pas. En ce qui concerne le premier paramètre, il faut utiliser la macro de conversion GTK_SCALE() .

    La deuxième fonction permet de savoir si le label est affiché ou pas.

    Ensuite nous avons la possibilité de gérer le nombre de chiffre après la virgule qui sont affichés :

    void gtk_scale_set_digits(GtkScale *scale, gint digits);
gint gtk_scale_get_digits(GtkScale *scale);

    Dans la première fonction, digit est bien sûr le nombre de chiffre après la virgule à afficher.

    Et pour terminer, nous allons voir comment gérer la position du label par rapport à la barre de sélection :

    void gtk_scale_set_value_pos(GtkScale *scale, GtkPositionType pos);
GtkPositionType gtk_scale_get_value_pos(GtkScale *scale);

    Le paramètre pos (de la première fonction) peut prendre une de ces quatre valeurs :

    • GTK_POS_LEFT pour le mettre à gauche ;
    • GTK_POS_RIGHT pour le mettre à droite ;
    • GTK_POS_TOP pour le mettre au-dessus ;
    • GTK_POS_BOTTOM pour le mettre en dessous.

    3.4 Exemple.

    Nous allons reprendre l'exemple précédent que nous allons transformer pour l'utilisation de widget GtkHScale ce qui le rendra plus simple. Cependant, pour utiliser les fonctions du widget GtkScale, le premier GtkHScale aura un label au-dessus, le second aura un label en dessous et le troisième pas du tout.

    XVII-C-5. 3.5 Programme exemple.

    #include <stdlib.h>
#include <gtk/gtk.h>
int main(int argc,char **argv)
{
   GtkWidget* pWindow;
   GtkWidget *pMainVBox;
   GtkWidget *pFrame;
   GtkWidget *pLabel;
   GtkWidget *pScale;
   gtk_init(&argc,&argv);
   pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_window_set_title(GTK_WINDOW(pWindow), "GtkScale");
   gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
   gtk_container_set_border_width(GTK_CONTAINER(pWindow), 4);
   pMainVBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pWindow), pMainVBox);
   pFrame = gtk_frame_new("Rouge");
   /* Création du widget GtkHScale */
   pScale = gtk_hscale_new_with_range(0, 255, 1);
   gtk_container_add(GTK_CONTAINER(pFrame), pScale);
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   pFrame = gtk_frame_new("Vert");
   pScale = gtk_hscale_new_with_range(0, 255, 1);
   /* Position du label en dessous */
   gtk_scale_set_value_pos(GTK_SCALE(pScale), GTK_POS_BOTTOM);
   gtk_container_add(GTK_CONTAINER(pFrame), pScale);
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   pFrame = gtk_frame_new("Bleu");
   pScale = gtk_hscale_new_with_range(0, 255, 1);
   /* Pas d'affichage du label */
   gtk_scale_set_draw_value(GTK_SCALE(pScale), FALSE);
   gtk_container_add(GTK_CONTAINER(pFrame), pScale);
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   gtk_widget_show_all(pWindow);
   g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
   gtk_main();
   return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    XVII-D. Le widget GtkSpinButton

    Ce widget est un peu différent des deux précédents dans le sens ou il s'agit en fait d'un widget GtkEntry et de deux GtkButton assemblés pour former un tout. De plus il permet de sélectionner une valeur soit en utilisant les boutons soit en entrant directement la valeur au clavier. Mais voyons tout cela en détail.

    XVII-D-1. Création

    Il existe là aussi deux fonctions de créations :

    GtkWidget* gtk_spin_button_new(GtkAdjustment *adjustment, gdouble climb_rate, guint digits);
GtkWidget* gtk_spin_button_new_with_range(gdouble min, gdouble max, gdouble step);

    La première fonction permet de créer un GtkSpinButton à partir d'un GtkAdjustment, mais il faut cependant préciser le paramètre climb_rate qui correspond au paramètre step_increment du GtkAdjustment et digits qui représente le nombre de chiffre après la virgule de la valeur que nous allons pouvoir sélectionner.

    La deuxième fonction permet de se passer de GtkAdjustment car nous fixons les valeurs minimales et maximales (paramètres min et max ) ainsi que la valeur de modification avec le paramètre step . En ce qui concerne le nombre de chiffres après la virgule, il sera identique à celui de step .

    Pour ce widget, le clavier permet aussi de modifier sa valeur. Les touches fléchées HAUT et BAS permettent de modifier la valeur de step_increment (dans le cas de l'utilisation d'un GtkAdjustment) ou de step . De plus les touches PAGE_UP et PAGE_DOWN permettent de modifier la valeur de page_increment avec une création utilisant un GtkAdjustment ou de step*10 dans l'autre cas.

    XVII-D-2. Gestion des valeurs

    Voyons tout d'abord comment récupérer ou modifier la valeur en cours d'un GtkSpinButton à l'aide d'une de ces trois fonctions :

    void gtk_spin_button_set_value(GtkSpinButton *spin_button, gdouble value);
gdouble gtk_spin_button_get_value(GtkSpinButton *spin_button);
gint gtk_spin_button_get_value_as_int(GtkSpinButton *spin_button);

    La première fonction permet donc de modifier la valeur d'un GtkSpinButton. Il faut pour le premier paramètre utiliser la macro de conversion GTK_SPIN_BUTTON() .

    La deuxième fonction permet de récupérer la valeur d'un GtkSpinButton tout simplement.

    La dernière fonction a le même but que la deuxième mais celle-ci renvoie un entier alors que la deuxième renvoie un double.

    Nous pouvons nous occuper du nombre de chiffre après la virgule avec ces deux fonctions :

    guint gtk_spin_button_get_digits(GtkSpinButton *spin_button);
void gtk_spin_button_set_digits(GtkSpinButton *spin_button, guint digits);

    La première fonction permettant de connaître le nombre de chiffre après la virgule alors que la seconde permet de le modifier.

    Et pour terminer voici les fonctions qui permettent de modifier les bornes des valeurs :

    void gtk_spin_button_get_range(GtkSpinButton *spin_button, gdouble *min, gdouble *max);
void gtk_spin_button_set_range(GtkSpinButton *spin_button, gdouble min, gdouble max);

    Comme d'habitude, la première fonction permet de connaître les bornes du GtkSpinButton et la seconde permet de les modifier.

    Mais bien sûr, tout cela peut se modifier à l'aide des GtkAdjustment avec ces deux fonctions :

    GtkAdjustment* gtk_spin_button_get_adjustment(GtkSpinButton *spin_button);
void gtk_spin_button_set_adjustment(GtkSpinButton *spin_button, GtkAdjustment *adjustment);

    XVII-D-3. Exemple

    Le programme exemple sera identique aux deux premiers, c'est à dire qu'il va permettre de sélectionner une valeur RGB à l'aide de trois widgets GtkSpinButton.

    XVII-D-4. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>
int main(int argc,char **argv)
{
   GtkWidget* pWindow;
   GtkWidget *pMainVBox;
   GtkWidget *pFrame;
   GtkWidget *pSpin;
   gtk_init(&argc,&argv);
   pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_window_set_title(GTK_WINDOW(pWindow), "GtkSpinButton");
   gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
   gtk_container_set_border_width(GTK_CONTAINER(pWindow), 4);
   pMainVBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pWindow), pMainVBox);
   pFrame = gtk_frame_new("Rouge");
   /* Création du widget GtkSpinButton */
   pSpin = gtk_spin_button_new_with_range(0, 255, 1);
   gtk_container_add(GTK_CONTAINER(pFrame), pSpin);
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   pFrame = gtk_frame_new("Vert");
   pSpin = gtk_spin_button_new_with_range(0, 255, 1);
   gtk_container_add(GTK_CONTAINER(pFrame), pSpin);
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   pFrame = gtk_frame_new("Bleu");
   pSpin = gtk_spin_button_new_with_range(0, 255, 1);
   gtk_container_add(GTK_CONTAINER(pFrame), pSpin);
   gtk_box_pack_start(GTK_BOX(pMainVBox), pFrame, FALSE, FALSE, 0);
   gtk_widget_show_all(pWindow);
   g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);
   gtk_main();
   return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible

    XVII-E. En savoir plus

    XVII-E-1. Signaux

    Prototypes fonctions callback :

    GtkAdjustment

    • changed

      • Ce signal est émis lorsqu'un paramètre autre que value est modifié.

        void user_function(GtkAdjustment *adjustment, gpointer user_data);

    • value-changed

      • Ce signal est émis lorsque le paramètre value est modifié.

        void user_function(GtkAdjustment *adjustment, gpointer user_data);

    GtkScale

    • format-value

      • gchar* user_function(GtkScale *scale, gdouble arg1, gpointer user_data);

    GtkRange

    • adjust-bounds

      • void user_function(GtkRange *range, gdouble arg1, gpointer user_data);

    • move-slider

      • void user_function(GtkRange *range, GtkScrollType arg1, gpointer user_data);

    • value-changed

      • void user_function(GtkRange *range, gpointer user_data);

    GtkSpinButton

    • change-value

      • void user_function(GtkSpinButton *spinbutton, GtkScrollType arg1, gpointer user_data);

    • input

      • gint user_function(GtkSpinButton *spinbutton, gpointer arg1, gpointer user_data);

    • output

      • gboolean user_function(GtkSpinButton *spinbutton, gpointer user_data);

    • value-changed

      • gboolean user_function(GtkSpinButton *spinbutton, gpointer user_data);

    XVII-E-2. Fonction non documentées

    GtkAdjustment

    void gtk_adjustment_changed(GtkAdjustment *adjustment);
void gtk_adjustment_value_changed(GtkAdjustment *adjustment);

    GtkRange

    GtkAdjustment* gtk_range_get_adjustment(GtkRange *range);
void gtk_range_set_update_policy(GtkRange *range, GtkUpdateType policy);
gboolean gtk_range_get_inverted(GtkRange *range);
void gtk_range_set_inverted(GtkRange *range, gboolean setting);
GtkUpdateType gtk_range_get_update_policy(GtkRange *range);

    GtkSpinButton

    void gtk_spin_button_configure(GtkSpinButton *spin_button, GtkAdjustment *adjustment, gdouble climb_rate, guint digits);
void gtk_spin_button_set_increments(GtkSpinButton *spin_button, gdouble step, gdouble page);
void gtk_spin_button_set_update_policy(GtkSpinButton *spin_button, GtkSpinButtonUpdatePolicy policy);
void gtk_spin_button_set_numeric(GtkSpinButton *spin_button, gboolean numeric);
void gtk_spin_button_spin(GtkSpinButton *spin_button, GtkSpinType direction, gdouble increment);
void gtk_spin_button_set_wrap(GtkSpinButton *spin_button, gboolean wrap);
void gtk_spin_button_set_snap_to_ticks(GtkSpinButton *spin_button, gboolean snap_to_ticks);
void gtk_spin_button_update(GtkSpinButton *spin_button);
void gtk_spin_button_get_increments(GtkSpinButton *spin_button, gdouble *step, gdouble *page);
gboolean gtk_spin_button_get_numeric(GtkSpinButton *spin_button);
gboolean gtk_spin_button_get_snap_to_ticks(GtkSpinButton *spin_button);
GtkSpinButtonUpdatePolicy gtk_spin_button_get_update_policy(GtkSpinButton *spin_button);
gboolean gtk_spin_button_get_wrap(GtkSpinButton *spin_button);


Cours GTK 2

Table des matières

  • XVIII. La barre de progression
    • XVIII-A. Utilisation d'une GtkProgressBar
      • XVIII-A-1. Création
      • XVIII-A-2. Modification de la progression
      • XVIII-A-3. Exemple.
      • XVIII-A-4. 1.4 Programme exemple
    • XVIII-B. Utilisation dans une boucle for
      • XVIII-B-1. Problème
      • XVIII-B-2. Solution
      • XVIII-B-3. Exemple
      • XVIII-B-4. Programme exemple
    • XVIII-C. En savoir plus
      • XVIII-C-1. Fonctions documentées

    • XVIII. La barre de progression

    Parfois un programme doit faire de gros calculs, et il est utile d'avertir l'utilisateur sur l'avancement du calcul afin qu'il soit au courant et ne pense pas que le programme est bloqué. Nous allons donc étudier le widget GtkProgressBar qui sert justement à cela.

    Image non disponible

    XVIII-A. Utilisation d'une GtkProgressBar

    XVIII-A-1. Création

    Ce widget étant comme tous les autres widgets de GTK+ sa fonction de création est très simple :

    GtkWidget* gtk_progress_bar_new(void)

    Voila, nous avons maintenant notre GtkProgressBar, maintenant vous avez besoin de ne connaître que peu de fonctions pour la faire marcher.

    XVIII-A-2. Modification de la progression

    Tout d'abord nous allons voir comment connaître la position de la GtkProgressBar :

    gdouble gtk_progress_bar_get_fraction(GtkProgressBar *pbar);

    Cette fonction renvoie un double qui sera compris entre 0.0 et 1.0, c'est-à-dire un pourcentage. Pour ce qui est du paramètre pbar , il faut utiliser la macro de conversion GTK_PROGRESS_BAR() .

    À l'inverse, pour fixer la position de la GtkProgressBar, la fonction est :

    void gtk_progress_bar_set_fraction(GtkProgressBar *pbar, gdouble fraction);

    Là aussi, le paramètre fraction doit être compris entre 0.0 et 1.0.

    XVIII-A-3. Exemple.

    Notre premier exemple, très simple, sera fait d'une fenêtre (bien entendu) avec à l'intérieur une barre de progression et un bouton qui permettra de faire avancer la barre de progression de 10%. Et lorsque la barre de progression sera à 100%, nous la réinitialiserons à 0%.

    XVIII-A-4. 1.4 Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

void OnButton(GtkWidget *pWidget, gpointer data);

int main(int argc,char **argv)
{
   GtkWidget* pWindow;
   GtkWidget *pMainVBox;
   GtkWidget *pProgress;
   GtkWidget *pButton;

   gtk_init(&argc,&argv);

   pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_window_set_title(GTK_WINDOW(pWindow), "GtkProgressBar");
   gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
   gtk_container_set_border_width(GTK_CONTAINER(pWindow), 4);

   pMainVBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pWindow), pMainVBox);

   /* Création de la barre de progression */
   pProgress = gtk_progress_bar_new();
   gtk_box_pack_start(GTK_BOX(pMainVBox), pProgress, TRUE, FALSE, 0);

   pButton = gtk_button_new_with_label("Ajouter 10%");
   gtk_box_pack_start(GTK_BOX(pMainVBox), pButton, TRUE, FALSE, 0);
   g_signal_connect_swapped(G_OBJECT(pButton), "clicked", G_CALLBACK(OnButton), pProgress);

   gtk_widget_show_all(pWindow);

   g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

   gtk_main();

   return EXIT_SUCCESS;
}

void OnButton(GtkWidget *pWidget, gpointer data)
{
   gdouble dFraction;

   /* Récupération de la valeur de la barre de progression */
   dFraction = gtk_progress_bar_get_fraction(GTK_PROGRESS_BAR(pWidget));

   dFraction += 0.1;

   if(dFraction > 1.0)
      dFraction = 0.0;

   /* Modification de la valeur de la barre de progression */
   gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(pWidget), dFraction);
}

    Résultat :

    Image non disponible

    XVIII-B. Utilisation dans une boucle for

    XVIII-B-1. Problème

    Si nous modifions légèrement le programme précédent pour insérer une boucle qui fera avancer automatiquement la barre de progression (par exemple une boucle de 2000 itérations) cela ne fonctionnera pas. La seule raison pour laquelle cela ne fonctionnera pas est que GTK+ ne reprend pas la main car le programme reste dans la boucle for alors GTK+ ne peut pas mettre à jour les widgets affichés.

    XVIII-B-2. Solution

    La solution, est de dire clairement à GTK+ qu'il doit remettre à jour grâce à la fonction suivante :

    gboolean gtk_main_iteration(void);

    Cette fonction permet de faire une itération comme son nom l'indique, une est une seul donc à ce moment là GTK+ va reprendre la main puis la rendre aussitôt, si bien qu'il pourra mettre à jour les widgets. Donc il suffit d'ajouter cette fonction après gtk_progress_bar_set_fraction pour faire fonctionner correctement notre programme.

    Ce problème étant réglé, nous allons faire face à un deuxième problème, ou plutôt pseudo-problème. En général, lorsque le programme fait un gros calcul, l'application doit être « bloquer » pendant ce temps. Donc tant que le traitement n'est pas fini, il faut éviter que l'utilisateur ne puisse change des données. Prenons le cas d'un correcteur d'orthographe, disons qu'il le fasse automatiquement. Pendant qu'il vérifie l'orthographe il serai bête que l'utilisateur puisse modifier le texte, ce qui fausserai alors toute la correction. Pourquoi cet exemple? Et bien le fait de rendre la main a GTK+ lui donne le pouvoir de traiter d'autres évènements, comme un clic de sourie et autres. Donc à tout moment pendant ce calcul l'utilisateur peut modifier quelque chose (ici pas grand choses si ce n'est que re-cliquer sur le bouton de démarrage de la boucle), donc il faut pouvoir l'empêcher de faire une quelconque action.

    Nous allons pouvoir bloquer l'utilisateur à l'aide de ces deux fonctions :

    void gtk_grab_add(GtkWidget *widget);
void gtk_grab_remove(GtkWidget *widget);

    Nous allons faire une description rapide de ces deux fonctions à l'aide d'un exemple. Prenons le cas ou l'utilisateur fait dans une application (de dessin par exemple) une sélection par glissement, quand il quitte la zone pour atterrir à coté voir en dehors de la fenêtre, la sélection continue, et bien c'est parce que l'application se focalise sur la fenêtre de sélection, c'est un peu ce que fait gtk_grab_add . Nous lui donnons un widget et seuls les événements de ce widget seront traités par GTK+, et cela tant que gtk_grab_remove n'a pas été invoqué. Si bien que quand l'utilisateur fait une sélection et qu'il passe au-dessus d'un autre widget, il est ignoré.

    Voilà maintenant nous avons tout pour que pendant la boucle la barre de progression soit remise à jour et sans que l'utilisateur ne puisse cliquer ailleurs.

    XVIII-B-3. Exemple

    Nous réutilisons donc l'exemple précédant en modifiant la fonction du bouton qui sert maintenant à démarrer la progression de la barre qui sera effectué dans la fonction callback.

    XVIII-B-4. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

void OnButton(GtkWidget *pWidget, gpointer data);

int main(int argc,char **argv)
{
   GtkWidget* pWindow;
   GtkWidget *pMainVBox;
   GtkWidget *pProgress;
   GtkWidget *pButton;

   gtk_init(&argc,&argv);

   pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
   gtk_window_set_title(GTK_WINDOW(pWindow), "GtkProgressBar");
   gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
   gtk_container_set_border_width(GTK_CONTAINER(pWindow), 4);

   pMainVBox = gtk_vbox_new(TRUE, 0);
   gtk_container_add(GTK_CONTAINER(pWindow), pMainVBox);

   /* Création de la barre de progression */
   pProgress = gtk_progress_bar_new();
   gtk_box_pack_start(GTK_BOX(pMainVBox), pProgress, TRUE, FALSE, 0);

   pButton = gtk_button_new_from_stock(GTK_STOCK_REFRESH);
   gtk_box_pack_start(GTK_BOX(pMainVBox), pButton, TRUE, FALSE, 0);
   g_signal_connect_swapped(G_OBJECT(pButton), "clicked", G_CALLBACK(OnButton), pProgress);

   gtk_widget_show_all(pWindow);

   g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

   gtk_main();

   return EXIT_SUCCESS;
}

void OnButton(GtkWidget *pWidget, gpointer data)
{
   gdouble dFraction;
   gint i;
   gint iTotal = 2000;

   /* Initialisation */
   gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(pWidget), 0.0);

   /* Ici on grab sur la barre de progression pour 2 raisons : */
   /* - cela évite a GTK+ de regarder tous les événements ce qui rend plus rapide */
   /* l'utilisation de gtk_main_iteration() */
   /* - on empêche toute action de l'utilisateur */
   gtk_grab_add(pWidget);

   for(i = 0 ; i < iTotal ; ++i)
   {
      dFraction = (gdouble)i / (gdouble)iTotal;

      /* Modification de la valeur de la barre de progression */
      gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(pWidget), dFraction);

      /* On donne la main a GTK+ */
      gtk_main_iteration();
   }

   /* On supprime le grab sur la barre de progression */
   gtk_grab_remove(pWidget);
}

    Résultat :

    Image non disponible

    XVIII-C. En savoir plus

    XVIII-C-1. Fonctions documentées

    void gtk_progress_bar_set_text(GtkProgressBar *pbar, const gchar *text);

    Permet d'afficher du texte dans la barre de progression.

    Entrée(s) :

    • pbar : objet GtkProgressBar.
    • text : texte à afficher.

    Sortie : rien.

    G_CONST_RETURN gchar* gtk_progress_bar_get_text(GtkProgressBar *pbar);

    Permet d'obtenir le texte affiché dans la barre de progression.

    Entrée(s) :

    • pbar : objet GtkProgressBar.

    Sortie : le texte affiché.

    void gtk_progress_bar_set_orientation(GtkProgressBar *pbar, GtkProgressBarOrientation orientation);

    Permet de définir le sens de progression de la GtkProgressBar.

    Entrée(s) :

    • pbar : objet GtkProgressBar.

    • orientation : une des valeurs suivantes

      • GTK_PROGRESS_LEFT_TO_RIGHT -> de gauche à droite ;
      • GTK_PROGRESS_RIGHT_TO_LEFT -> de droite à gauche ;
      • GTK_PROGRESS_BOTTOM_TO_TOP -> de bas en haut ;
      • GTK_PROGRESS_TOP_TO_BOTTOM -> de haut en bas.

    Sortie : rien

    GtkProgressBarOrientation gtk_progress_bar_get_orientation(GtkProgressBar *pbar);

    Permet d'obtenir le sens de progression de la GtkProgressBar.

    Entrée(s) :

    • pbar : objet GtkProgressBar.

    Sortie : le sens de progression.


Cours GTK 2

Table des matières

  • XIX. Sélection de fichier
    • XIX-A. Créer une gtk_file_selection et l'utiliser
      • XIX-A-1. 1.1 Créer le widget
      • XIX-A-2. Affichage
      • XIX-A-3. 1.3 Récupérer le chemin.
      • XIX-A-4. Programme d'exemple
    • XIX-B. En savoir plus
      • XIX-B-1. Fonctions documentées

    • XIX. Sélection de fichier

    Le widget GtkFileSelection est une boîte de dialogue qui permet de demander à l'utilisateur le chemin d'un fichier particulier, à travers une liste de répertoire et de fichiers.

    Image non disponible

    XIX-A. Créer une gtk_file_selection et l'utiliser

    Nous allons ici créer une gtk_file_selection, et apprendre comment récupérer le chemin entré par l'utilisateur.

    XIX-A-1. 1.1 Créer le widget

    On crée tout d'abord un pointeur vers la structure GtkWidget.

    GtkWidget* file_selection;

    Maintenant, on va instancier ce pointeur grâce à la fonction

    GtkWidget* gtk_file_selection_new(const gchar* title)

    Cette fonction renvoie l'adresse d'une structure GtkFileSelection. L'argument « const gchar *title » est le titre de votre boîte de dialogue : vous pouvez ainsi l'appeler suivant vos besoins « ouvrir… », « sélectionner un fichier… », etc.

    XIX-A-2. Affichage

    Tout simplement :

    gtk_window_show(file_selection);

    Qui permet d'afficher la fenêtre et en même temps ce qu'elle contient (donc le label).

    XIX-A-3. 1.3 Récupérer le chemin.

    Notre but est de récupérer le chemin spécifié par l'utilisateur lorsque celui-ci appuie sur le bouton ok.

    On va pour cela tout d'abord utiliser le callback suivant, qui lorsque le bouton sera appuyé, appellera la fonction recuperer_chemin(GtkWidget *bouton, GtkWidget *file_selection) :

    g_signal_connect(GTK_FILE_SELECTION(file_selection)->ok_button, "clicked", G_CALLBACK(recuperer_chemin), file_selection );

    On voit ici que le bouton ok est une donnée membre de la structure GtkFileSelection : ok_button. Ce callback va fournir à la fonction recuperer_chemin un pointeur file_selection pour lui permettre de récupérer le chemin. Voila la fonction qui permet d'obtenir le chemin du fichier ou du répertoire sélectionné à partir de la gtk_file_selection :

    G_CONST_RETURN gchar* gtk_file_selection_get_filename(GtkFileSelection *filesel);

    Voyons maintenant comment nous pouvons l'utiliser dans notre fonction :

    void recuperer_chemin(GtkWidget *bouton, GtkWidget *file_selection)
{
    gchar *chemin;

    chemin = gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selection) );
}

    Enfin, il est nécessaire de détruire la boîte de dialogue lorsque l'utilisateur clique sur le bouton cancel, et lorsque le chemin a été récupéré. Pour ce faire, faisons les deux callbacks suivants après le callback précédent :

    g_signal_connect_swapped(GTK_FILE_SELECTION(file_selection)->ok_button, "clicked", G_CALLBACK(gtk_widget_destroy), file_selection);
g_signal_connect_swapped(GTK_FILE_SELECTION(file_selection)->cancel_button, "clicked", G_CALLBACK(gtk_widget_destroy), file_selection);

    Le fait d'appeler g_signal_connect_swapped va permettre de ne pas transmettre l'argument inhérent au signal clicked sur les boutons à gtk_widget_destroy, mais seulement l'argument file_selection. Notez que l'appel des fonctions callbacks se fait dans l'ordre de leur définition, ainsi le callback entre le ok_button et la fonction gtk_widget_destroy se fera après l'appel de la fonction recuperer_chemin. Lorsque la fenêtre sera détruite, le chemin sera récupéré.

    XIX-A-4. Programme d'exemple

    #include <gtk/gtk.h>

void quitter(GtkWidget *widget); void creer_file_selection();
void recuperer_chemin(GtkWidget *bouton, GtkWidget *file_selection);

int main(int argc,char* argv[])
{
    GtkWidget *win;
    GtkWidget *bouton_explorer;

    gtk_init(&argc,&argv);

    win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(win),"GtkFileSelection");
    gtk_window_set_default_size(GTK_WINDOW(win),320,200);

    // On crée un bouton explorer
    bouton_explorer=gtk_button_new_with_mnemonic("_Explorer...");
    // on met bouton_explorer dans win
    gtk_container_add(GTK_CONTAINER(win),bouton_explorer);

    g_signal_connect(G_OBJECT(win),"destroy",G_CALLBACK(quitter), NULL);
    g_signal_connect(G_OBJECT(bouton_explorer), "clicked", G_CALLBACK(creer_file_selection), NULL);

    // affichage de win et de ce qu'il contient
    gtk_widget_show_all(win);
    gtk_main();
    return 0;
}

void quitter(GtkWidget* widget)
{
    // destruction de win et de tout ce qu'il contient
    gtk_widget_destroy(widget);
    gtk_main_quit();
}

void creer_file_selection()
{
    GtkWidget *selection;

    selection = gtk_file_selection_new( g_locale_to_utf8( "  un fichier", -1, NULL, NULL, NULL) );
    gtk_widget_show(selection);

    //On interdit l'utilisation des autres fenêtres.
    gtk_window_set_modal(GTK_WINDOW(selection), TRUE);

    g_signal_connect(G_OBJECT(GTK_FILE_SELECTION(selection)->ok_button), "clicked", G_CALLBACK(recuperer_chemin), selection );

    g_signal_connect_swapped(G_OBJECT(GTK_FILE_SELECTION(selection)->cancel_button), "clicked", G_CALLBACK(gtk_widget_destroy), selection);
}

void recuperer_chemin(GtkWidget *bouton, GtkWidget *file_selection)
{
    const gchar* chemin;
    GtkWidget *dialog;
    chemin = gtk_file_selection_get_filename(GTK_FILE_SELECTION (file_selection) );

    dialog = gtk_message_dialog_new(GTK_WINDOW(file_selection),
    GTK_DIALOG_MODAL,
    GTK_MESSAGE_INFO,
    GTK_BUTTONS_OK,
    "Vous avez choisi :\n%s", chemin);

    gtk_dialog_run(GTK_DIALOG(dialog));
    gtk_widget_destroy(dialog);
    gtk_widget_destroy(file_selection);
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XIX-B. En savoir plus

    XIX-B-1. Fonctions documentées

    void gtk_file_selection_set_filename(GtkFileSelection *filesel,const gchar *filename);

    Permet de définir le chemin par défaut.

    Entrée(s) :

    • filesel : la GtkFileSelection.
    • filename : le chemin.

    Sortie : rien.

    void gtk_file_selection_hide_fileop_buttons(GtkFileSelection *filesel);

    Permet de cacher certains boutons de la boîte de dialogue(nouveau dossier, effacer le fichier, renommer le fichier

    Entrée(s) :

    • filesel : la GtkFileSelection.

    Sortie : rien

    void gtk_file_selection_show_fileop_buttons(GtkFileSelection *filesel);

    Permet d'afficher certains boutons de la boîte de dialogue(nouveau dossier, effacer le fichier, renommer le fichier).

    Entrée(s) :

    • filesel : la GtkFileSelection.

    Sortie : rien.

    void gtk_file_selection_set_select_multiple(GtkFileSelection *filesel,gboolean select_multiple);

    On définit si l'on peut selectionner plusieurs fichiers.

    Entrée(s) :

    • filesel : objet GtkFileSelection.
    • select_multiple : TRUE pour une selection multiple, FALSE (par défaut) sinon.

    Sortie : rien.

    gchar** gtk_file_selection_get_selections(GtkFileSelection *filesel);

    L'équivalent de gtk_file_selection_get_filename mais pour des sélections multiples.

    Entrée(s) :

    • filesel : objet GtkFileSelection.

    Sortie : un tableau contenant les différents chemins.


Cours GTK 2

Table des matières

  • XX. Les fenêtres avec barres de défilement
    • XX-A. Création et utilisation d'une GtkScrolledWindow
      • XX-A-1. Création
      • XX-A-2. Ajout du widget enfant.
      • XX-A-3. Affichage des barres de défilement.
      • XX-A-4. 1.4 Construction du programme exemple.
      • XX-A-5. 1.5 Programme exemple.
    • XX-B. En savoir plus
      • XX-B-1. Signaux
      • XX-B-2. Fonctions documentées

    • XX. Les fenêtres avec barres de défilement

    Nous allons cette fois voir un widget qui permet d'ajouter des barres de défilement au widget qui sera son enfant. Il s'agit d'un widget container qui s'appelle GtkScrolledWindow.

    Image non disponible

    XX-A. Création et utilisation d'une GtkScrolledWindow

    XX-A-1. Création

    Comme vous pouvez vous y attendre, la fonction de création et comme toutes les autres, c'est-à-dire prévisible.

    GtkWidget* gtk_scrolled_window_new(GtkAdjustment *hadjustment, GtkAdjustment *vadjustment);

    Les paramètres hadjustment et vadjustment permettent de définir les propriétés des barres de défilement. L'objet GtkAdjustment permet dans notre cas, de définir (entre autres) la valeur minimale que peut prendre la barre de défilement, le pas de celle-ci.

    Mais à moins de connaître exactement tous ces paramètres, il vaut mieux laisser faire Gtk, et mettre hadjustment et vadjustment à NULL.

    XX-A-2. Ajout du widget enfant.

    Pour cela, il y a deux cas différents. Si par exemple le widget enfant a la capacité d'avoir ses propres barres de défilements, il suffit d'utiliser la fonction gtk_container_add.

    Par contre, dans le cas ou le widget ne possède pas cette capacité, il faudra utiliser cette fonction :

    void gtk_scrolled_window_add_with_viewport(GtkScrolledWindow *scrolled_window, GtkWidget *child);

    Avec cette fonction, Gtk va créer ce qu'il s'appelle un viewport (qui peut avec des barres de défilement) et va insérer l'enfant dans ce viewport.

    Seuls les widgets GtkTreeView, GtkTextView, et GtkLayout ont la possibilité d'avoir des barres défilements. Pour les autres widgets (comme les GtkHBox, GtkVBox…) il faudra utiliser la deuxième fonction.

    XX-A-3. Affichage des barres de défilement.

    Les barres de défilement s'affichent automatiquement (que ce soit la barre horizontale ou verticale). Il se peut que, par exemple, vous n'ayez besoin que de la barre verticale. Dans ce cas vous ne voulez sûrement pas avoir de barre de défilement horizontale. La solution à votre problème est cette fonction :

    void gtk_scrolled_window_set_policy(GtkScrolledWindow *scrolled_window, GtkPolicyType hscroll_policy, GtkPolicyType vscroll_policy);

    Les paramètres hscroll_policy et vscroll_policy peuvent prendre trois paramètres :

    • GTK_POLICY_ALWAYS : la barre de défilement est toujours affichée ;
    • GTK_POLICY_AUTOMATIC : la barre de défilement n'est affichée que si elle est utile ;
    • GTK_POLICY_NEVER : la barre de défilement n'est jamais affichée.

    Au contraire, pour savoir si elles sont affichées ou pas, il y a la fonction :

    void gtk_scrolled_window_get_policy(GtkScrolledWindow *scrolled_window, GtkPolicyType *hscroll_policy, GtkPolicyType *vscroll_policy);

    XX-A-4. 1.4 Construction du programme exemple.

    Notre exemple est très simple. Il consiste uniquement à afficher 10 labels les uns au-dessus des autres dans une fenêtre qui n'excédera pas une taille de 320x200. Voyons à quoi ressemble notre fenêtre si elle n'utilise pas les barres de défilement.

    Image non disponible

    Et voila le résultat, la fenêtre ne respecte pas la condition TAILLE = 320x200. Il faut utiliser le widget GtkScrolledWindow.

    Après avoir créé notre fenêtre principale, il faut créer notre widget et l'insérer dans la fenêtre :

    scrollbar = gtk_scrolled_window_new(NULL, NULL);
gtk_container_add(GTK_CONTAINER(window),scrollbar);

    Ensuite les labels seront contenus dans une GtkVBox qu'il faut insérer ainsi dans notre GtkScrolledWindow :

    gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrollbar), box);

    Puisque nous n'avons pas besoin de la barre de défilement horizontale, on la supprime :

    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollbar), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);

    Et voila notre toute nouvelle fenêtre est créé.

    XX-A-5. 1.5 Programme exemple.

    #include <gtk/gtk.h>
#include <stdio.h>

int main(int argc, char* argv[])
{
    GtkWidget* window;
    GtkWidget* box;
    GtkWidget *scrollbar;
    int i;

    gtk_init(&argc, &argv);
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(window), 320, 200);
    gtk_window_set_title(GTK_WINDOW(window), "GtkScrolledWindow");
    g_signal_connect(G_OBJECT(window),"destroy",G_CALLBACK(gtk_main_quit),0);

    scrollbar = gtk_scrolled_window_new(NULL, NULL);
    gtk_container_add(GTK_CONTAINER(window),scrollbar);

    box=gtk_vbox_new(FALSE,5);
    gtk_scrolled_window_add_with_viewport(GTK_SCROLLED_WINDOW(scrollbar), box);

    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollbar), GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);

    for(i = 1 ; i <= 10 ; ++i)
    {
        GtkWidget *label;
        char texte[10];

        sprintf(texte, "Label %d", i);

        label = gtk_label_new(texte);

        gtk_box_pack_start(GTK_BOX(box), label, FALSE, FALSE, 5);
    }

    gtk_widget_show_all(window);

    gtk_main();

    return 0;
}

    Résultat :

    Image non disponible
    Image non disponible

    XX-B. En savoir plus

    XX-B-1. Signaux

    Prototypes fonctions callback :

    • move-focus-out

      • void user_function(GtkScrolledWindow *scrolledwindow, GtkDirectionType arg1, gpointer user_data);

    • scroll-child

      • void user_function(GtkScrolledWindow *scrolledwindow, GtkScrollType arg1, gboolean arg2, gpointer user_data);

    XX-B-2. Fonctions documentées

    GtkAdjustment* gtk_scrolled_window_get_hadjustment(GtkScrolledWindow *scrolled);

    Pour obtenir les paramètres de la barre de défilement horizontale.

    Entrée(s) :

    • scrolled : la fenêtre.

    Sortie : un pointeur sur la structure GtkAdjustment contenant les paramètres.

    GtkAdjustment* gtk_scrolled_window_get_vadjustment(GtkScrolledWindow *scrolled);

    Pour obtenir les paramètres de la barre de défilement verticale.

    Entrée(s) :

    • scrolled : la fenêtre.

    Sortie : un pointeur sur la structure GtkAdjustment contenant les paramètres.

    void gtk_scrolled_window_set_hadjustment(GtkScrolledWindow *scrolled, GtkAdjustment *hadjustment);

    Définit les paramètres de la barre de défilement horizontale.

    Entrée(s) :

    • scrolled : la fenêtre.
    • hadjustment : structure contenant les paramètres à appliquer.

    Sortie : rien.

    void gtk_scrolled_window_set_vadjustment(GtkScrolledWindow *scrolled, GtkAdjustment *vadjustment);

    Définit les paramètres de la barre de défilement verticale.

    Entrée(s) :

    • scrolled : la fenêtre.
    • vadjustment : structure contenant les paramètres à appliquer.

    Sortie : rien.

    void gtk_scrolled_window_set_shadow_type(GtkScrolledWindow *scrolled, GtkShadowType type);

    Modifie l'aspect du bord du widget enfant à la GtkScrolledWindow.

    Entrée(s) :

    • scrolled : la fenêtre.

    • type : une valeur parmi

      • GTK_SHADOW_NONE ;
      • GTK_SHADOW_IN ;
      • GTK_SHADOW_OUT ;
      • GTK_SHADOW_ETCHED_IN ;
      • GTK_SHADOW_ETCHED_OUT.

    Sortie : rien.

    GtkShadowType gtk_scrolled_window_get_shadow_type(GtkScrolledWindow *scrolled);

    Pour connaître l'aspect du bord du widget enfant.

    Entrée(s) :

    • scrolled : la fenêtre.

    Sortie : type de l'aspect.

    void gtk_scrolled_window_set_placement(GtkScrolledWindow *scrolled, GtkCornerType window_placement);

    Définit la position du widget enfant par rapport aux barres de défilement.

    Entrée(s) :

    • scrolled : la fenêtre.

    • window_placement : la position du widget, peut être

      • GTK_CORNER_TOP_LEFT -> en dessus à gauche ;
      • GTK_CORNER_BOTTOM_LEFT -> en dessous à gauche ;
      • GTK_CORNER_TOP_RIGHT -> en dessus à droite ;
      • GTK_CORNER_BOTTOM_RIGHT -> en dessous à droite.

    Sortie : rien.

    GtkCornerType gtk_scrolled_window_get_placement(GtkScrolledWindow *scrolled);

    Pour obtenir la position du widget enfant par rapport aux barres de défilement.

    Entrée(s) :

    • scrolled : la fenêtre.

    Sortie : la position du widget.


Cours GTK 2

Table des matières

  • XXI. Les zones de texte
    • XXI-A. Créer une application saisissant un paragraphe.
      • XXI-A-1. Afficher un GtkTextView
      • XXI-A-2. Accéder au contenu de GtkTextBuffer
      • XXI-A-3. Programme exemple
    • XXI-B. Affichage du contenu d'un fichier
      • XXI-B-1. Construction de l'application.
      • XXI-B-2. 2.2 Programme exemple.
    • XXI-C. En savoir plus
      • XXI-C-1. Les signaux
      • XXI-C-2. Fonctions non documentées

    • XXI. Les zones de texte

    Nous avons appris à saisir une ligne tapée par l'utilisateur grâce à GtkEntry mais comment fait-on pour saisir plusieurs lignes (comme le font les traitements de texte, etc.). Et bien nous utilisons non pas un mais trois widgets !

    GtkTextBuffer : Ce widget non-graphique sert à stocker les données. C'est tout simplement un buffer !

    GtkTextView : Ce widget sert à afficher le contenu d'un GtkTextBuffer. Il s'occupe de tout ce qui est graphique : il gère les événements de l'utilisateur, les insertions de caractères, etc.

    GtkTextIter : Ce widget non-graphique désigne une position dans GtkTextBuffer. Il permet de manipuler GtkTextBuffer.

    Image non disponible

    XXI-A. Créer une application saisissant un paragraphe.

    XXI-A-1. Afficher un GtkTextView

    On va maintenant initialiser un pointeur de type GtkWidget* qui pointera vers la structure du GtkTextView :

    GtkWidget* text_view=gtk_text_view_new();

    Là, nous venons de créer un GtkTextView mais également un GtkTextBuffer. Si nous voulons donner notre propre buffer, nous utiliserons la fonction GtkWidget* gtk_text_view_new_with_buffer(GtkTextBuffer* buffer) qui reçoit en argument l'adresse de notre buffer.

    Il faut maintenant insérer text_view dans la box.

    gtk_box_pack_start(GTK_BOX(box),text_view,TRUE,TRUE,0);

    C bon ! On peut commencer à compiler pour voir le résultat et même commencer à saisir du texte dedans (mais ça ne sert strictement à rien :-).

    XXI-A-2. Accéder au contenu de GtkTextBuffer

    Tout d'abord, on va créer un bouton qui dès qu'il sera cliqué par l'utilisateur, on saisira le contenu du buffer :

    button=gtk_button_new_with_label("Valider");
gtk_box_pack_start(GTK_BOX(box),button,false,false,0);
g_signal_connect(G_OBJECT(button),"clicked",G_CALLBACK(saisie),text_view);

    Pendant la connection du signal, on envoie text_view pour pouvoir travailler dessus dans la fonction callback :

    void saisie(GtkButton *button, gpointer user_data)
{
}

    Ce qui suit est à insérer dans la fonction.

    Comme on n'a pas donné « notre » GtkTextBuffer pendant la création de text_view , GTK+ a donné un buffer par défaut. Pour récupérer l'adresse de ce buffer, on utilise la fonction

    GtkTextBuffer* gtk_text_view_get_buffer(GtkTextView* text_view)

    On va bien sûr recevoir la valeur renvoyée par la fonction dans un pointeur de type GtkTextBuffer* et non pas GtkWidget* :

    text_buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(user_data));

    Enregistrer la macro de conversion au passage…

    Maintenant, reste plus qu'à extraire les données contenues dans text_buffer et les mettre dans un gchar* par exemple. On utilise la fonction

    gchar* gtk_text_buffer_get_text(GtkTextBuffer *buffer, const GtkTextIter *start, const GtkTextIter *end, gboolean include_hidden_chars)

    Où là ! C'est quoi tous ces paramètres ! On va les traiter un par un :

    • buffer : pas de commentaire
    • start : la position de début de la chaîne de caractère
    • end : la position de fin de la chaîne de caractère
    • include_hidden_chars : on verra plus tard :-) (mettre false)
    • valeur retournée : une chaîne de caractère de type C et allouée dans le tas

    Mais à quoi peut bien servir le deuxième et le troisième paramètre ? Et bien imaginez que vous ne vouliez qu'une partie du buffer, et bien ces deux paramètres indiquent la position de début et de fin de la chaîne de caractère dans le buffer. On appelle cela une sous-chaîne de caractère.

    Mais comment ça s'initialise les GtkTextIter ?

    On ne va pas déclarer des pointeurs comme on a l'habitude de le faire mais des variables directo. Nous voulons recevoir tout le buffer. Les itérateurs doivent être les itérateurs de début et de fin de text_buffer . Pour ce faire, nous utiliserons les deux fonctions :

    void gtk_text_buffer_get_start_iter(GtkTextBuffer *buffer, GtkTextIter *iter)
void gtk_text_buffer_get_end_iter(GtkTextBuffer *buffer, GtkTextIter *iter)

    On va donc initialiser nos deux itérateurs comme cela :

    gtk_text_buffer_get_start_iter(text_buffer,&start);
gtk_text_buffer_get_end_iter(text_buffer,&end);

    À noter que nous n'avons pas besoin de la macro de conversion GTK_TEXT_BUFFER() pour text_buffer car c'est déjà un pointeur de type GtkTextBuffer* et ne pas oublier le & avant start et end car ce ne sont pas des pointeurs. Et la fonction reçoit leur adresse.

    Maintenant, on va utiliser la fonction gtk_text_buffer_get_text :

    buf=gtk_text_buffer_get_text(text_buffer,&start,&end,false);

    Et voilà ! Nous avons le contenu de text_buffer copié dans buf . On peut faire ce qu'on veut avec maintenant : le copier dans un fichier, l'afficher dans un autre widget, etc.

    Il ne faut surtout pas oublier d'utiliser la fonction g_free() qui est la version GTK+ de la fonction free du C pour libérer la mémoire allouée dans le tas par buf :

    g_free(buf);

    XXI-A-3. Programme exemple

    #include <gtk\gtk.h>

gboolean fin(GtkWidget *widget, GdkEvent *event, gpointer user_data);
void saisie(GtkButton *button, GtkWidget *view);

int main(int argc, char* argv[])
{
    GtkWidget* window;
    GtkWidget* box;
    GtkWidget* text_view;
    GtkWidget* button;

    gtk_init(&argc, &argv);
    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(window), 320, 200);
    gtk_window_set_title(GTK_WINDOW(window), "XIII. Les zones de texte.");
    g_signal_connect(G_OBJECT(window),"destroy",G_CALLBACK(gtk_main_quit),0);

    box=gtk_vbox_new(FALSE,5);
    gtk_container_add(GTK_CONTAINER(window),box);

    text_view=gtk_text_view_new();
    gtk_box_pack_start(GTK_BOX(box),text_view,TRUE,TRUE,0);

    button=gtk_button_new_with_label("Afficher");
    gtk_box_pack_start(GTK_BOX(box),button,FALSE,FALSE,0);
    g_signal_connect(G_OBJECT(button),"clicked",G_CALLBACK(saisie),text_view);

    gtk_widget_show_all(window);

    gtk_main();

    return 0;
}

gboolean fin(GtkWidget *widget, GdkEvent *event, gpointer user_data)
{
    gtk_widget_destroy(widget);
    gtk_main_quit();

    return 0;
}

void saisie(GtkButton *button, GtkWidget *view)
{
    GtkWidget *dialog;
    GtkTextBuffer* text_buffer=0;
    GtkTextIter start;
    GtkTextIter end;
    gchar* buf=0;

    //On récupère le buffer
    text_buffer=gtk_text_view_get_buffer(GTK_TEXT_VIEW(view));
    //On récupère l'origine du buffer
    gtk_text_buffer_get_start_iter(text_buffer,&start);
    //On récupère la fin du buffer
    gtk_text_buffer_get_end_iter(text_buffer,&end);

    //On copie le contenu du buffer dans une variable
    buf=gtk_text_buffer_get_text(text_buffer,&start, &end,TRUE);

    //On affiche le texte dans une boite de dialogue.
    dialog = gtk_message_dialog_new(NULL,
    GTK_DIALOG_MODAL,
    GTK_MESSAGE_INFO,
    GTK_BUTTONS_OK,
    "Votre texte :\n%s", buf);

    gtk_dialog_run(GTK_DIALOG(dialog));
    gtk_widget_destroy(dialog);

    //On libere la memoire
    g_free(buf);
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XXI-B. Affichage du contenu d'un fichier

    Maintenant, nous allons utiliser ce que nous avons vu dans les deux précédents chapitres afin de créer une application ouvrant un fichier et affichant son contenu.

    XXI-B-1. Construction de l'application.

    Nous avons déjà vu la quasi-totalité des fonctions que nous allons utiliser pour ce programme, cela va donc aller assez vite. Nous allons étudier uniquement la partie du code qui a pour fonction de lire le fichier et d'afficher le texte.

    Dans un premier temps, nous allons récupérer le buffer de notre GtkTextView.

    buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));

    Ensuite on récupère le chemin du fichier et on l'ouvre en mode texte :

    chemin = gtk_file_selection_get_filename(GTK_FILE_SELECTION(file_selection));
fichier = fopen(chemin,"rt");

    Si le fichier ne c'est pas ouvert correctement, on affiche bien sur un message d'erreur.

    On vide ensuite le contenu du buffer avec cette fonction.

    void gtk_text_buffer_delete(GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end);

    Il faut bien sur récupérer auparavent les valeurs start et end comme pour le premier exemple.

    gtk_text_buffer_delete(buffer, &start, &end);

    Ensuite, le procédé consiste à lire les lignes du fichier une par une et à les ajouter au buffer avec la fonction :

    void gtk_text_buffer_insert(GtkTextBuffer *buffer, GtkTextIter *iter, const gchar *text, gint len);

    Le paramètre iter est la position du buffer à partir de laquelle le texte text va être ajouté. Il faut donc avant chaque insertion récupérer la position finale du buffer pour que le texte se place correctement. Voici la boucle qui nous permet de créer notre buffer.

    while(fgets(lecture, 1024, fichier))
{
    gtk_text_buffer_get_end_iter(buffer,&end);
    gtk_text_buffer_insert(buffer, &end, g_locale_to_utf8(lecture, -1, NULL, NULL, NULL), -1);
}

    Et voila le code source est complet.

    XXI-B-2. 2.2 Programme exemple.

    #include <gtk/gtk.h>
#include <stdio.h>

static GtkWidget *text_view;

void saisie(GtkButton *button);
void ouvrir_fichier(GtkWidget *bouton, GtkWidget *file_selection);

int main(int argc, char* argv[])
{
    GtkWidget* window;
    GtkWidget* box;
    GtkWidget* button;
    GtkWidget *scrollbar;

    gtk_init(&argc, &argv);

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(window), 480, 480);
    gtk_window_set_title(GTK_WINDOW(window), "XIII. Les zones de texte.");
    g_signal_connect(G_OBJECT(window),"destroy",G_CALLBACK(gtk_main_quit),0);

    box=gtk_vbox_new(FALSE,5);
    gtk_container_add(GTK_CONTAINER(window),box);

    scrollbar = gtk_scrolled_window_new(NULL, NULL);
    gtk_box_pack_start(GTK_BOX(box), scrollbar, TRUE, TRUE, 5);

    text_view=gtk_text_view_new();
    gtk_container_add(GTK_CONTAINER(scrollbar),text_view);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrollbar), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

    button=gtk_button_new_with_label(g_locale_to_utf8( "  un fichier", -1, NULL, NULL, NULL));
    gtk_box_pack_start(GTK_BOX(box),button,FALSE,FALSE,0);
    g_signal_connect(G_OBJECT(button),"clicked",G_CALLBACK(saisie),NULL);

    gtk_widget_show_all(window);

    gtk_main();

    return 0;
}

void saisie(GtkButton *button)
{
    GtkWidget *selection;

    selection = gtk_file_selection_new( g_locale_to_utf8( "  un fichier", -1, NULL, NULL, NULL) );
    gtk_widget_show(selection);

    g_signal_connect(G_OBJECT(GTK_FILE_SELECTION(selection)->ok_button), "clicked", G_CALLBACK(ouvrir_fichier), selection );

    g_signal_connect_swapped(G_OBJECT(GTK_FILE_SELECTION(selection)->cancel_button), "clicked", G_CALLBACK(gtk_widget_destroy), selection);
}

void ouvrir_fichier(GtkWidget *bouton, GtkWidget *file_selection)
{
    GtkTextBuffer *buffer;
    GtkTextIter start;
    GtkTextIter end;
    FILE *fichier;
    const gchar *chemin;
    gchar lecture[1024];

    buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));

    chemin = gtk_file_selection_get_filename(GTK_FILE_SELECTION (file_selection));
    fichier = fopen(chemin,"rt");

    if(fichier == NULL)
    {
        GtkWidget *dialog;

        dialog = gtk_message_dialog_new(GTK_WINDOW(file_selection), GTK_DIALOG_MODAL, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Impossible d'ouvrir le fichier : \n%s", g_locale_to_utf8(chemin, -1, NULL, NULL, NULL));
        gtk_dialog_run(GTK_DIALOG(dialog));
        gtk_widget_destroy(dialog);
        gtk_widget_destroy(file_selection);

        return;
    }

    gtk_widget_destroy(file_selection);

    gtk_text_buffer_get_start_iter(buffer,&start);
    gtk_text_buffer_get_end_iter(buffer,&end);
    gtk_text_buffer_delete(buffer, &start, &end);

    while(fgets(lecture, 1024, fichier))
    {
        gtk_text_buffer_get_end_iter(buffer,&end);
        gtk_text_buffer_insert(buffer, &end, g_locale_to_utf8(lecture, -1, NULL, NULL, NULL), -1);
    }

    fclose(fichier);
}

    Résultat :

    Image non disponible

    XXI-C. En savoir plus

    XXI-C-1. Les signaux

    Prototypes fonctions callback :

    GtkTextView

    • copy-clipboard

      • void user_function(GtkTextView *textview, gpointer user_data);

    • cut-clipboard

      • void user_function(GtkTextView *textview, gpointer user_data);

    • delete-from-cursor

      • void user_function(GtkTextView *textview, GtkDeleteType arg1, gint arg2, gpointer user_data);

    • insert-at-cursor

      • void user_function(GtkTextView *textview, gchar *arg1, gpointer user_data);

    • move-cursor

      • void user_function(GtkTextView *textview, GtkMovementStep arg1, gint arg2, gboolean arg3, gpointer user_data);

    • move-focus

      • void user_function(GtkTextView *textview, GtkDirectionType arg1, gpointer user_data);

    • page-horizontally

      • void user_function(GtkTextView *textview, gint arg1, gboolean arg2, gpointer user_data);

    • paste-clipboard

      • void user_function(GtkTextView *textview, gpointer user_data);

    • populate-popup

      • void user_function(GtkTextView *textview, GtkMenu *arg1, gpointer user_data);

    • set-anchor

      • void user_function(GtkTextView *textview, gpointer user_data);

    • set-scroll-adjustments

      • void user_function(GtkTextView *textview, GtkAdjustment *arg1, GtkAdjustment *arg2, gpointer user_data);

    • toggle-overwrite

      • void user_function(GtkTextView *textview, gpointer user_data);

    GtkTextBuffer :

    • apply-tag

      • void user_function(GtkTextBuffer *textbuffer, GtkTextTag *arg1, GtkTypeTextIter arg2, GtkTypeTextIter arg3, gpointer user_data);

    • begin-user-action

      • void user_function(GtkTextBuffer *textbuffer, gpointer user_data);

    • changed

      • void user_function(GtkTextBuffer *textbuffer, gpointer user_data);

    • delete-range

      • void user_function(GtkTextBuffer *textbuffer, GtkTypeTextIter arg1, GtkTypeTextIter arg2, gpointer user_data);

    • end-user-action

      • void user_function(GtkTextBuffer *textbuffer, gpointer user_data);

    • insert-child-anchor

      • void user_function(GtkTextBuffer *textbuffer, GtkTypeTextIter arg1, GtkTextChildAnchor *arg2, gpointer user_data);

    • insert-pixbuf

      • void user_function(GtkTextBuffer *textbuffer, GtkTypeTextIter arg1, GdkPixbuf *arg2, gpointer user_data);

    • insert-text

      • void user_function(GtkTextBuffer *textbuffer, GtkTypeTextIter arg1, gchar *arg2, gint arg3, gpointer user_data);

    • mark-deleted

      • void user_function(GtkTextBuffer *textbuffer, GtkTextMark *arg1, gpointer user_data);

    • mark-set

      • void user_function(GtkTextBuffer *textbuffer, GtkTypeTextIter arg1, GtkTextMark *arg2, gpointer user_data);

    • modified-changed

      • void user_function(GtkTextBuffer *textbuffer, gpointer user_data);

    • remove-tag

      • void user_function(GtkTextBuffer *textbuffer, GtkTextTag *arg1, GtkTypeTextIter arg2, GtkTypeTextIter arg3, gpointer user_data);

    XXI-C-2. Fonctions non documentées

    GtkTextView :

    GtkWidget* gtk_text_view_new_with_buffer(GtkTextBuffer *buffer);
void gtk_text_view_set_buffer(GtkTextView *text_view, GtkTextBuffer *buffer);
GtkTextBuffer* gtk_text_view_get_buffer(GtkTextView *text_view);
void gtk_text_view_scroll_to_mark(GtkTextView *text_view, GtkTextMark *mark, gdouble within_margin, gboolean use_align, gdouble xalign, gdouble yalign);
gboolean gtk_text_view_scroll_to_iter(GtkTextView *text_view, GtkTextIter *iter, gdouble within_margin, gboolean use_align, gdouble xalign, gdouble yalign);
void gtk_text_view_scroll_mark_onscreen(GtkTextView *text_view, GtkTextMark *mark);
gboolean gtk_text_view_move_mark_onscreen(GtkTextView *text_view, GtkTextMark *mark);
gboolean gtk_text_view_place_cursor_onscreen(GtkTextView *text_view);
void gtk_text_view_get_visible_rect(GtkTextView *text_view, GdkRectangle *visible_rect);
void gtk_text_view_get_iter_location(GtkTextView *text_view, const GtkTextIter *iter, GdkRectangle *location);
void gtk_text_view_get_line_at_y(GtkTextView *text_view, GtkTextIter *target_iter, gint y, gint *line_top);
void gtk_text_view_get_line_yrange(GtkTextView *text_view, const GtkTextIter *iter, gint *y, gint *height);
void gtk_text_view_get_iter_at_location(GtkTextView *text_view, GtkTextIter *iter, gint x, gint y);
void gtk_text_view_buffer_to_window_coords(GtkTextView *text_view, GtkTextWindowType win, gint buffer_x, gint buffer_y, gint *window_x, gint *window_y);
void gtk_text_view_window_to_buffer_coords(GtkTextView *text_view, GtkTextWindowType win, gint window_x, gint window_y, gint *buffer_x, gint *buffer_y);
GdkWindow* gtk_text_view_get_window(GtkTextView *text_view, GtkTextWindowType win);
GtkTextWindowType gtk_text_view_get_window_type(GtkTextView *text_view, GdkWindow *window);
void gtk_text_view_set_border_window_size(GtkTextView *text_view, GtkTextWindowType type, gint size);
gint gtk_text_view_get_border_window_size(GtkTextView *text_view, GtkTextWindowType type);
gboolean gtk_text_view_forward_display_line(GtkTextView *text_view, GtkTextIter *iter);
gboolean gtk_text_view_backward_display_line(GtkTextView *text_view, GtkTextIter *iter);
gboolean gtk_text_view_forward_display_line_end(GtkTextView *text_view, GtkTextIter *iter);
gboolean gtk_text_view_backward_display_line_start(GtkTextView *text_view, GtkTextIter *iter);
gboolean gtk_text_view_starts_display_line(GtkTextView *text_view, const GtkTextIter *iter);
gboolean gtk_text_view_move_visually(GtkTextView *text_view, GtkTextIter *iter, gint count);
void gtk_text_view_add_child_at_anchor(GtkTextView *text_view, GtkWidget *child, GtkTextChildAnchor *anchor);
GtkTextChildAnchor* gtk_text_child_anchor_new(void);
GList* gtk_text_child_anchor_get_widgets(GtkTextChildAnchor *anchor);
gboolean gtk_text_child_anchor_get_deleted(GtkTextChildAnchor *anchor);
void gtk_text_view_add_child_in_window(GtkTextView *text_view, GtkWidget *child, GtkTextWindowType which_window, gint xpos, gint ypos);
void gtk_text_view_move_child(GtkTextView *text_view, GtkWidget *child, gint xpos, gint ypos);
void gtk_text_view_set_wrap_mode(GtkTextView *text_view, GtkWrapMode wrap_mode);
GtkWrapMode gtk_text_view_get_wrap_mode(GtkTextView *text_view);
void gtk_text_view_set_editable(GtkTextView *text_view, gboolean setting);
gboolean gtk_text_view_get_editable(GtkTextView *text_view);
void gtk_text_view_set_cursor_visible(GtkTextView *text_view, gboolean setting);
gboolean gtk_text_view_get_cursor_visible(GtkTextView *text_view);
void gtk_text_view_set_pixels_above_lines(GtkTextView *text_view, gint pixels_above_lines);
gint gtk_text_view_get_pixels_above_lines(GtkTextView *text_view);
void gtk_text_view_set_pixels_below_lines(GtkTextView *text_view, gint pixels_below_lines);
gint gtk_text_view_get_pixels_below_lines(GtkTextView *text_view);
void gtk_text_view_set_pixels_inside_wrap(GtkTextView *text_view, gint pixels_inside_wrap);
gint gtk_text_view_get_pixels_inside_wrap(GtkTextView *text_view);
void gtk_text_view_set_justification(GtkTextView *text_view, GtkJustification justification);
GtkJustification gtk_text_view_get_justification(GtkTextView *text_view);
void gtk_text_view_set_left_margin(GtkTextView *text_view, gint left_margin);
gint gtk_text_view_get_left_margin(GtkTextView *text_view);
void gtk_text_view_set_right_margin(GtkTextView *text_view, gint right_margin);
gint gtk_text_view_get_right_margin(GtkTextView *text_view);
void gtk_text_view_set_indent(GtkTextView *text_view, gint indent);
gint gtk_text_view_get_indent(GtkTextView *text_view);
void gtk_text_view_set_tabs(GtkTextView *text_view, PangoTabArray *tabs);
PangoTabArray* gtk_text_view_get_tabs(GtkTextView *text_view);
GtkTextAttributes* gtk_text_view_get_default_attributes(GtkTextView *text_view);

    GtkTextBuffer :

    GtkTextBuffer* gtk_text_buffer_new(GtkTextTagTable *table);
gint gtk_text_buffer_get_line_count(GtkTextBuffer *buffer);
gint gtk_text_buffer_get_char_count(GtkTextBuffer *buffer);
GtkTextTagTable* gtk_text_buffer_get_tag_table(GtkTextBuffer *buffer);
void gtk_text_buffer_insert_at_cursor(GtkTextBuffer *buffer, const gchar *text, gint len);
gboolean gtk_text_buffer_insert_interactive(GtkTextBuffer *buffer, GtkTextIter *iter, const gchar *text, gint len, gboolean default_editable);
gboolean gtk_text_buffer_insert_interactive_at_cursor(GtkTextBuffer *buffer, const gchar *text, gint len, gboolean default_editable);
void gtk_text_buffer_insert_range(GtkTextBuffer *buffer, GtkTextIter *iter, const GtkTextIter *start, const GtkTextIter *end);
gboolean gtk_text_buffer_insert_range_interactive(GtkTextBuffer *buffer, GtkTextIter *iter, const GtkTextIter *start, const GtkTextIter *end, gboolean default_editable);
void gtk_text_buffer_insert_with_tags(GtkTextBuffer *buffer, GtkTextIter *iter, const gchar *text, gint len, GtkTextTag *first_tag, ...);
void gtk_text_buffer_insert_with_tags_by_name(GtkTextBuffer *buffer, GtkTextIter *iter, const gchar *text, gint len, const gchar *first_tag_name, ...);
gboolean gtk_text_buffer_delete_interactive(GtkTextBuffer *buffer, GtkTextIter *start_iter, GtkTextIter *end_iter, gboolean default_editable);
void gtk_text_buffer_set_text(GtkTextBuffer *buffer, const gchar *text, gint len);
gchar* gtk_text_buffer_get_slice(GtkTextBuffer *buffer, const GtkTextIter *start, const GtkTextIter *end, gboolean include_hidden_chars);
void gtk_text_buffer_insert_pixbuf(GtkTextBuffer *buffer, GtkTextIter *iter, GdkPixbuf *pixbuf);
void gtk_text_buffer_insert_child_anchor(GtkTextBuffer *buffer, GtkTextIter *iter, GtkTextChildAnchor *anchor);
GtkTextChildAnchor* gtk_text_buffer_create_child_anchor(GtkTextBuffer *buffer, GtkTextIter *iter);
GtkTextMark* gtk_text_buffer_create_mark(GtkTextBuffer *buffer, const gchar *mark_name, const GtkTextIter *where, gboolean left_gravity);
void gtk_text_buffer_move_mark(GtkTextBuffer *buffer, GtkTextMark *mark, const GtkTextIter *where);
void gtk_text_buffer_move_mark_by_name(GtkTextBuffer *buffer, const gchar *name, const GtkTextIter *where);
void gtk_text_buffer_delete_mark(GtkTextBuffer *buffer, GtkTextMark *mark);
void gtk_text_buffer_delete_mark_by_name(GtkTextBuffer *buffer, const gchar *name);
GtkTextMark* gtk_text_buffer_get_mark(GtkTextBuffer *buffer, const gchar *name);
GtkTextMark* gtk_text_buffer_get_insert(GtkTextBuffer *buffer);
GtkTextMark* gtk_text_buffer_get_selection_bound(GtkTextBuffer *buffer);
void gtk_text_buffer_place_cursor(GtkTextBuffer *buffer, const GtkTextIter *where);
void gtk_text_buffer_apply_tag(GtkTextBuffer *buffer, GtkTextTag *tag, const GtkTextIter *start, const GtkTextIter *end);
void gtk_text_buffer_remove_tag(GtkTextBuffer *buffer, GtkTextTag *tag, const GtkTextIter *start, const GtkTextIter *end);
void gtk_text_buffer_apply_tag_by_name(GtkTextBuffer *buffer, const gchar *name, const GtkTextIter *start, const GtkTextIter *end);
void gtk_text_buffer_remove_tag_by_name(GtkTextBuffer *buffer, const gchar *name, const GtkTextIter *start, const GtkTextIter *end);
void gtk_text_buffer_remove_all_tags(GtkTextBuffer *buffer, const GtkTextIter *start, const GtkTextIter *end);
GtkTextTag* gtk_text_buffer_create_tag(GtkTextBuffer *buffer, const gchar *tag_name, const gchar *first_property_name, ...);
void gtk_text_buffer_get_iter_at_line_offset(GtkTextBuffer *buffer, GtkTextIter *iter, gint line_number, gint char_offset);
void gtk_text_buffer_get_iter_at_offset(GtkTextBuffer *buffer, GtkTextIter *iter, gint char_offset);
void gtk_text_buffer_get_iter_at_line(GtkTextBuffer *buffer, GtkTextIter *iter, gint line_number);
void gtk_text_buffer_get_iter_at_line_index(GtkTextBuffer *buffer, GtkTextIter *iter, gint line_number, gint byte_index);
void gtk_text_buffer_get_iter_at_mark(GtkTextBuffer *buffer, GtkTextIter *iter, GtkTextMark *mark);
void gtk_text_buffer_get_iter_at_child_anchor(GtkTextBuffer *buffer, GtkTextIter *iter, GtkTextChildAnchor *anchor);
void gtk_text_buffer_get_bounds(GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end);
gboolean gtk_text_buffer_get_modified(GtkTextBuffer *buffer);
void gtk_text_buffer_set_modified(GtkTextBuffer *buffer, gboolean setting);
gboolean gtk_text_buffer_delete_selection(GtkTextBuffer *buffer, gboolean interactive, gboolean default_editable);
void gtk_text_buffer_paste_clipboard(GtkTextBuffer *buffer, GtkClipboard *clipboard, GtkTextIter *override_location, gboolean default_editable);
void gtk_text_buffer_copy_clipboard(GtkTextBuffer *buffer, GtkClipboard *clipboard);
void gtk_text_buffer_cut_clipboard(GtkTextBuffer *buffer, GtkClipboard *clipboard, gboolean default_editable);
gboolean gtk_text_buffer_get_selection_bounds(GtkTextBuffer *buffer, GtkTextIter *start, GtkTextIter *end);
void gtk_text_buffer_begin_user_action(GtkTextBuffer *buffer);
void gtk_text_buffer_end_user_action(GtkTextBuffer *buffer);
void gtk_text_buffer_add_selection_clipboard(GtkTextBuffer *buffer, GtkClipboard *clipboard);
void gtk_text_buffer_remove_selection_clipboard(GtkTextBuffer *buffer, GtkClipboard *clipboard);

    GtkTextIter :

    GtkTextBuffer* gtk_text_iter_get_buffer(const GtkTextIter *iter);
GtkTextIter* gtk_text_iter_copy(const GtkTextIter *iter);
void gtk_text_iter_free(GtkTextIter *iter);
gint gtk_text_iter_get_offset(const GtkTextIter *iter);
gint gtk_text_iter_get_line(const GtkTextIter *iter);
gint gtk_text_iter_get_line_offset(const GtkTextIter *iter);
gint gtk_text_iter_get_line_index(const GtkTextIter *iter);
gint gtk_text_iter_get_visible_line_index(const GtkTextIter *iter);
gint gtk_text_iter_get_visible_line_offset(const GtkTextIter *iter);
gunichar gtk_text_iter_get_char(const GtkTextIter *iter);
gchar* gtk_text_iter_get_slice(const GtkTextIter *start, const GtkTextIter *end);
gchar* gtk_text_iter_get_text(const GtkTextIter *start, const GtkTextIter *end);
gchar* gtk_text_iter_get_visible_slice(const GtkTextIter *start, const GtkTextIter *end);
gchar* gtk_text_iter_get_visible_text(const GtkTextIter *start, const GtkTextIter *end);
GdkPixbuf* gtk_text_iter_get_pixbuf(const GtkTextIter *iter);
GSList* gtk_text_iter_get_marks(const GtkTextIter *iter);
GSList* gtk_text_iter_get_toggled_tags(const GtkTextIter *iter, gboolean toggled_on);
GtkTextChildAnchor* gtk_text_iter_get_child_anchor(const GtkTextIter *iter);
gboolean gtk_text_iter_begins_tag(const GtkTextIter *iter, GtkTextTag *tag);
gboolean gtk_text_iter_ends_tag(const GtkTextIter *iter, GtkTextTag *tag);
gboolean gtk_text_iter_toggles_tag(const GtkTextIter *iter, GtkTextTag *tag);
gboolean gtk_text_iter_has_tag(const GtkTextIter *iter, GtkTextTag *tag);
GSList* gtk_text_iter_get_tags(const GtkTextIter *iter);
gboolean gtk_text_iter_editable(const GtkTextIter *iter, gboolean default_setting);
gboolean gtk_text_iter_can_insert(const GtkTextIter *iter, gboolean default_editability);
gboolean gtk_text_iter_starts_word(const GtkTextIter *iter);
gboolean gtk_text_iter_ends_word(const GtkTextIter *iter);
gboolean gtk_text_iter_inside_word(const GtkTextIter *iter);
gboolean gtk_text_iter_starts_line(const GtkTextIter *iter);
gboolean gtk_text_iter_ends_line(const GtkTextIter *iter);
gboolean gtk_text_iter_starts_sentence(const GtkTextIter *iter);
gboolean gtk_text_iter_ends_sentence(const GtkTextIter *iter);
gboolean gtk_text_iter_inside_sentence(const GtkTextIter *iter);
gboolean gtk_text_iter_is_cursor_position(const GtkTextIter *iter);
gint gtk_text_iter_get_chars_in_line(const GtkTextIter *iter);
gint gtk_text_iter_get_bytes_in_line(const GtkTextIter *iter);
gboolean gtk_text_iter_get_attributes(const GtkTextIter *iter, GtkTextAttributes *values);
PangoLanguage* gtk_text_iter_get_language(const GtkTextIter *iter);
gboolean gtk_text_iter_is_end(const GtkTextIter *iter);
gboolean gtk_text_iter_is_start(const GtkTextIter *iter);
gboolean gtk_text_iter_forward_char(GtkTextIter *iter);
gboolean gtk_text_iter_backward_char(GtkTextIter *iter);
gboolean gtk_text_iter_forward_chars(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_backward_chars(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_forward_line(GtkTextIter *iter);
gboolean gtk_text_iter_backward_line(GtkTextIter *iter);
gboolean gtk_text_iter_forward_lines(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_backward_lines(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_forward_word_ends(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_backward_word_starts(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_forward_word_end(GtkTextIter *iter);
gboolean gtk_text_iter_backward_word_start(GtkTextIter *iter);
gboolean gtk_text_iter_forward_cursor_position(GtkTextIter *iter);
gboolean gtk_text_iter_backward_cursor_position(GtkTextIter *iter);
gboolean gtk_text_iter_forward_cursor_positions(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_backward_cursor_positions(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_backward_sentence_start(GtkTextIter *iter);
gboolean gtk_text_iter_backward_sentence_starts(GtkTextIter *iter, gint count);
gboolean gtk_text_iter_forward_sentence_end(GtkTextIter *iter);
gboolean gtk_text_iter_forward_sentence_ends(GtkTextIter *iter, gint count);
void gtk_text_iter_set_offset(GtkTextIter *iter, gint char_offset);
void gtk_text_iter_set_line(GtkTextIter *iter, gint line_number);
void gtk_text_iter_set_line_offset(GtkTextIter *iter, gint char_on_line);
void gtk_text_iter_set_line_index(GtkTextIter *iter, gint byte_on_line);
void gtk_text_iter_set_visible_line_index(GtkTextIter *iter, gint byte_on_line);
void gtk_text_iter_set_visible_line_offset(GtkTextIter *iter, gint char_on_line);
void gtk_text_iter_forward_to_end(GtkTextIter *iter);
gboolean gtk_text_iter_forward_to_line_end(GtkTextIter *iter);
gboolean gtk_text_iter_forward_to_tag_toggle(GtkTextIter *iter, GtkTextTag *tag);
gboolean gtk_text_iter_backward_to_tag_toggle(GtkTextIter *iter, GtkTextTag *tag);
gboolean (*GtkTextCharPredicate)(gunichar ch, gpointer user_data);
gboolean gtk_text_iter_forward_find_char(GtkTextIter *iter, GtkTextCharPredicate pred, gpointer user_data, const GtkTextIter *limit);
gboolean gtk_text_iter_backward_find_char(GtkTextIter *iter, GtkTextCharPredicate pred, gpointer user_data, const GtkTextIter *limit);
gboolean gtk_text_iter_forward_search(const GtkTextIter *iter, const gchar *str, GtkTextSearchFlags flags, GtkTextIter *match_start, GtkTextIter *match_end, const GtkTextIter *limit);
gboolean gtk_text_iter_backward_search(const GtkTextIter *iter, const gchar *str, GtkTextSearchFlags flags, GtkTextIter *match_start, GtkTextIter *match_end, const GtkTextIter *limit);
gboolean gtk_text_iter_equal(const GtkTextIter *lhs, const GtkTextIter *rhs);
gint gtk_text_iter_compare(const GtkTextIter *lhs, const GtkTextIter *rhs);
gboolean gtk_text_iter_in_range(const GtkTextIter *iter, const GtkTextIter *start, const GtkTextIter *end);
void gtk_text_iter_order(GtkTextIter *first, GtkTextIter *second);


Cours GTK 2

Table des matières

  • XXII. Les pages à onglets
    • XXII-A. Utilisation du widget GtkNotebook.
      • XXII-A-1. 1.1 Création.
      • XXII-A-2. Insertion de pages.
      • XXII-A-3. Gestion des pages.
      • XXII-A-4. Gestion des labels.
      • XXII-A-5. Modification des propriétés du GtkNotebook
      • XXII-A-6. Exemple
      • XXII-A-7. Programme exemple
    • XXII-B. Ajouter un menu de navigation
      • XXII-B-1. Création.
      • XXII-B-2. Gestion des labels.
      • XXII-B-3. Exemple.
      • XXII-B-4. Programme exemple.
    • XXII-C. En savoir plus
      • XXII-C-1. Les signaux
      • XXII-C-2. Fonctions non documentées

    • XXII. Les pages à onglets

    Nous allons étudier dans ce chapitre les pages à onglets qui sont souvent utilisés dans les fenêtres de configuration. Pour cela nous allons utiliser le widget GtkNotebook qui dérive du widget GtkContainer.

    Image non disponible

    XXII-A. Utilisation du widget GtkNotebook.

    XXII-A-1. 1.1 Création.

    Pour ne pas changer la création du widget en lui-même est très simple :

    GtkWidget* gtk_notebook_new(void);

    Maintenant que notre widget est créé, nous allons lui ajouter des pages.

    XXII-A-2. Insertion de pages.

    Nous avons là aussi trois fonctions qui restent très classiques :

    void gtk_notebook_append_page(GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label);
void gtk_notebook_prepend_page(GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label);
void gtk_notebook_insert_page(GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label, gint position);

    La première fonction ajoute une nouvelle page à onglet à la suite des autres, la deuxième fonction l'ajoute au début, et la dernière l'ajoute à une position particulière.

    Le premier paramètre, notebook , est bien sûr le GtkNotebook dans lequel nous voulons ajouter la page. Il faudra utiliser la macro de conversion GTK_NOTEBOOK() .

    Le second, child , est le widget qui sera inséré dans la nouvelle page, et le troisième ( tab_label ) le widget qui sera affiché dans l'onglet.

    La troisième fonction demande un paramètre supplémentaire ( position ) qui est la position à laquelle il faut ajouter la page. Si la valeur n'est pas correcte (négative ou trop grande) la page sera ajoutée à la suite des autres.

    XXII-A-3. Gestion des pages.

    Nous allons maintenant voir les fonctions qui permettent de connaître le nombre de page, la page en cours et d'autres fonctions.

    Tout d'abord, la fonction suivante permet de connaître le nombre total de pages contenus dans le GtkNotebook :

    gint gtk_notebook_get_n_pages(GtkNotebook *notebook);

    Ensuite, pour connaître le numéro de la page courante, nous avons cette fonction :

    gint gtk_notebook_get_current_page(GtkNotebook *notebook);

    Par contre pour connaître le numéro d'une autre page que la courante, il faudra utiliser cette fonction :

    gint gtk_notebook_page_num(GtkNotebook *notebook, GtkWidget *child);

    Cette fonction permet d'obtenir le numéro de la page contenant le widget child , ce qui impose donc d'avoir gardé une trace de ce widget pour pouvoir utiliser cette fonction.

    Avec ce numéro de page, nous allons pouvoir récupérer un pointeur sur le widget qui est contenu dans la page avec cette fonction :

    GtkWidget* gtk_notebook_get_nth_page(GtkNotebook *notebook, gint page_num);

    Ou bien nous allons pouvoir tout simplement supprimer la page :

    void gtk_notebook_remove_page(GtkNotebook *notebook, gint page_num);

    Et pour terminer, voici trois fonctions de navigation :

    void gtk_notebook_next_page(GtkNotebook *notebook);
void gtk_notebook_prev_page(GtkNotebook *notebook);
void gtk_notebook_set_current_page(GtkNotebook *notebook, gint page_num);

    La première fonction passe de la page courante à la page suivante. Si la dernière page est déjà affichée, il ne se passera rien.

    La deuxième fonction, elle, passe de la page courante à la page précédente. Bien entendu, si la page courante est déjà la première cette fonction n'aura aucun effet.

    La dernière fonction quant à elle passe directement à une page précise. Si la valeur de page_num est négative, nous passerons à la dernière page, par contre si cette valeur est trop grande, aucune action ne sera faite.

    XXII-A-4. Gestion des labels.

    Nous allons maintenant voir comment modifier ou récupérer le label d'une page. Attention pour chacune de ces fonctions, il faut connaître le widget contenu dans la page.

    Tout d'abord, pour récupérer le label de la page, il existe deux fonctions différentes :

    G_CONST_RETURN gchar* gtk_notebook_get_tab_label_text(GtkNotebook *notebook, GtkWidget *child);
GtkWidget* gtk_notebook_get_tab_label(GtkNotebook *notebook, GtkWidget *child);

    La première fonction renvoie directement le label de la page sous la forme d'un gchar* . Cette fonction ne fonctionnera correctement uniquement si le label de la page est un GtkLabel, car comme nous l'avons lors de l'étude des fonctions d'insertion des pages, il est possible de définir le label comme autre chose qu'un GtkLabel. Dans ce cas, il faut utiliser la deuxième fonction qui elle renvoie le widget qui a été utilisé lors de la création.

    Pour définir un nouveau label pour une page, il existe là aussi deux fonctions :

    void gtk_notebook_set_tab_label_text(GtkNotebook *notebook, GtkWidget *child, const gchar *tab_text);
void gtk_notebook_set_tab_label(GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label);

    La première fonction permet de définir un label de type GtkLabel. Cette fonction s'occupe de la création du GtkLabel et de son insertion dans l'onglet de la page. La deuxième fonction permet d'insérer n'importe quel widget dans l'onglet, comme pour les fonctions de création. Dans les deux cas, si les paramètres tab_text ou tab_label sont définis comme NULL, le label de la page sera « Page y », y étant le numéro de la page.

    XXII-A-5. Modification des propriétés du GtkNotebook

    Nous allons maintenant voir comment modifier trois des propriétés d'un GtkNotebook. Tout d'abord voyons comment gérer la position des onglets :

    void gtk_notebook_set_tab_pos(GtkNotebook *notebook, GtkPositionType pos);
GtkPositionType gtk_notebook_get_tab_pos(GtkNotebook *notebook);

    La première fonction permet de modifier la position des onglets. C'est le paramètre pos qui définit sa position, et il doit prendre une des valeurs suivantes :

    • GTK_POS_LEFT pour les mettre à gauche ;
    • GTK_POS_RIGHT pour les mettre à droite ;
    • GTK_POS_TOP pour les mettre en haut ;
    • GTK_POS_BOTTOM pour les mettre en bas.

    La deuxième fonction permet au contraire de connaître la position des onglets. La valeur de retour est obligatoirement une des quatre valeurs précédentes.

    Ensuite nous allons pouvoir définir si les onglets doivent s'afficher ou pas avec ces fonctions :

    void gtk_notebook_set_show_tabs(GtkNotebook *notebook, gboolean show_tabs);
gboolean gtk_notebook_get_show_tabs(GtkNotebook *notebook);

    Si nous voulons modifier ce paramètre, il faut utiliser la première fonction en mettant le paramètre show_tabs à TRUE pour les afficher et à FALSE dans le cas contraire. La deuxième fonction permet quant à elle de connaître l'état de cette propriété.

    Et enfin, pour terminer nous allons voir comment ajouter deux boutons de navigation à la fin des onglets, pour le cas où il y aurait trop d'onglets à afficher :

    void gtk_notebook_set_scrollable(GtkNotebook *notebook, gboolean scrollable);
gboolean gtk_notebook_get_scrollable(GtkNotebook *notebook);

    Si nous mettons le paramètre scrollable de la première fonction à TRUE, deux boutons de navigation apparaîtront et une partie seulement des onglets sera affichée. Pour accéder aux autres onglets il faudra utiliser ces nouveaux boutons.

    Bien entendu, la deuxième fonction permet de savoir si les boutons de navigation sont présents ou pas.

    XXII-A-6. Exemple

    Nous allons créer un programme qui insèrera un GtkNotebook de 8 pages dans une fenêtre. Chaque page contiendra uniquement un label « Je suis le GtkLabel numero x » . Il y a aussi un bouton qui permettra d'afficher les informations de la page sélectionnée dans une boîte de dialogue.

    XXII-A-7. Programme exemple

    #include <stdlib.h>
#include <gtk/gtk.h>

void OnButton(GtkWidget *pButton, gpointer data);

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pNotebook;
    GtkWidget *pButton;
    gint i;

    gtk_init(&argc,&argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkNotebook");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    pButton = gtk_button_new_with_label("Informations");
    gtk_box_pack_start(GTK_BOX(pVBox), pButton, FALSE, FALSE, 0);

    /* Creation du GtkNotebook */
    pNotebook = gtk_notebook_new();
    gtk_box_pack_start(GTK_BOX(pVBox), pNotebook, TRUE, TRUE, 0);
    /* Position des onglets : en bas */
    gtk_notebook_set_tab_pos(GTK_NOTEBOOK(pNotebook), GTK_POS_BOTTOM);
    /* Ajout des boutons de navigation */
    gtk_notebook_set_scrollable(GTK_NOTEBOOK(pNotebook), TRUE);

    for(i = 0 ; i < 8 ; ++i)
    {
        GtkWidget *pLabel;
        GtkWidget *pTabLabel;
        gchar *sLabel;
        gchar *sTabLabel;

        sLabel = g_strdup_printf("Je suis le GtkLabel numero %d", i);
        sTabLabel = g_strdup_printf("Page %d", i);

        /* Creation des differents GtkLabel */
        pLabel = gtk_label_new(sLabel);
        pTabLabel = gtk_label_new(sTabLabel);

        /* Insertion de la page */
        gtk_notebook_append_page(GTK_NOTEBOOK(pNotebook), pLabel, pTabLabel);

        g_free(sLabel);
        g_free(sTabLabel);
    }

    g_signal_connect(G_OBJECT(pButton), "clicked", G_CALLBACK(OnButton), pNotebook);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

void OnButton(GtkWidget *pButton, gpointer data)
{
    GtkWidget *pDialog;
    GtkWidget *pChild;
    gint iPageNum;
    const gchar *sLabel;
    const gchar *sTabLabel;
    gchar *sDialogText;

    /* Recuperation de la page active */
    iPageNum = gtk_notebook_get_current_page(GTK_NOTEBOOK(data));
    /* Recuperation du widget enfant */
    pChild = gtk_notebook_get_nth_page(GTK_NOTEBOOK(data), iPageNum);

    /* Recuperation du label */
    sLabel = gtk_label_get_text(GTK_LABEL(pChild));
    /* Recuperation du label de l'onglet */
    sTabLabel = gtk_notebook_get_tab_label_text(GTK_NOTEBOOK(data), pChild);

    /* Creation du label de la boite de dialogue */
    sDialogText = g_strdup_printf("C'est la page %d\n"
        "Le label est \"%s\"\n"
        "Le label de l'onglet est \"%s\"\n",
        iPageNum,
        sLabel,
        sTabLabel);

    pDialog = gtk_message_dialog_new(NULL,
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        sDialogText);

    gtk_dialog_run(GTK_DIALOG(pDialog));

    gtk_widget_destroy(pDialog);

    g_free(sDialogText);
}

    Résultat :

    Image non disponible
    Image non disponible
    Image non disponible

    XXII-B. Ajouter un menu de navigation

    Nous allons maintenant voir comment ajouter un menu qui apparaîtra lorsque nous ferons un clic droit de la sourie sur une page du GtkNotebook. Cela ne se fait pas par le biais du widget GtkMenu mais directement avec les fonctions de GtkNotebook.

    XXII-B-1. Création.

    Pour avoir un GtkNotebook avec un menu de navigation, trois nouvelles fonctions de création sont disponibles :

    void gtk_notebook_append_page_menu(GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label, GtkWidget *menu_label);
void gtk_notebook_prepend_page_menu(GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label, GtkWidget *menu_label);
void gtk_notebook_insert_page_menu(GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label, GtkWidget *menu_label, gint position);

    La majorité des paramètres de ces trois fonctions sont identiques sauf pour le paramètre menu_label qui est le widget qui sera affiché dans le menu de navigation. Le fonctionnement de ces fonctions est le même que pour les fonctions sans menu.

    Ensuite pour activer ou désactiver la possibilité d'afficher le menu il faut utiliser une de ces fonctions :

    void gtk_notebook_popup_enable(GtkNotebook *notebook);
void gtk_notebook_popup_disable(GtkNotebook *notebook);

    La première fonction permet d'afficher le menu, et la deuxième au contraire empêche le menu de s'afficher.

    XXII-B-2. Gestion des labels.

    Comme pour les onglets, il existe des fonctions pour gérer les labels du menu. Pour récupérer le widget ou directement le texte du menu, nous avons ces deux fonctions :

    GtkWidget* gtk_notebook_get_menu_label(GtkNotebook *notebook, GtkWidget *child);
G_CONST_RETURN gchar* gtk_notebook_get_menu_label_text(GtkNotebook *notebook, GtkWidget *child);

    La première fonction renvoie donc le widget, quel qu'il soit, qui est à l'intérieur du menu alors que la seconde renvoie le texte du menu si l'élément est de type GtkLabel.

    Et pour changer le label du menu, il existe ces deux fonctions :

    void gtk_notebook_set_menu_label(GtkNotebook *notebook, GtkWidget *child, GtkWidget *menu_label);
void gtk_notebook_set_menu_label_text(GtkNotebook *notebook, GtkWidget *child, const gchar *menu_text);

    La première accepte n'importe quel type de widget, tandis que la seconde crée elle-même un widget de type GtkLabel avec comme texte la valeur du paramètre menu_text .

    XXII-B-3. Exemple.

    Nous allons reprendre le même exemple que pour la première partie en rajoutant le menu de navigation.

    XXII-B-4. Programme exemple.

    #include <stdlib.h>
#include <gtk/gtk.h>

void OnButton(GtkWidget *pButton, gpointer data);

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pVBox;
    GtkWidget *pNotebook;
    GtkWidget *pButton;
    gint i;

    gtk_init(&argc,&argv);

    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkNotebook");
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    pVBox = gtk_vbox_new(FALSE, 0);
    gtk_container_add(GTK_CONTAINER(pWindow), pVBox);

    pButton = gtk_button_new_with_label("Informations");
    gtk_box_pack_start(GTK_BOX(pVBox), pButton, FALSE, FALSE, 0);

    /* Creation du GtkNotebook */
    pNotebook = gtk_notebook_new();
    gtk_box_pack_start(GTK_BOX(pVBox), pNotebook, TRUE, TRUE, 0);
    /* Position des onglets : en bas */
    gtk_notebook_set_tab_pos(GTK_NOTEBOOK(pNotebook), GTK_POS_BOTTOM);
    /* Ajout des boutons de navigation */
    gtk_notebook_set_scrollable(GTK_NOTEBOOK(pNotebook), TRUE);

    for(i = 0 ; i < 8 ; ++i)
    {
        GtkWidget *pLabel;
        GtkWidget *pTabLabel;
        GtkWidget *pMenuLabel;
        gchar *sLabel;
        gchar *sTabLabel;
        gchar *sMenuLabel;

        sLabel = g_strdup_printf("Je suis le GtkLabel numero %d", i);
        sTabLabel = g_strdup_printf("Page %d", i);
        sMenuLabel = g_strdup_printf("Menu -> Page %d", i);

        /* Creation des differents GtkLabel */
        pLabel = gtk_label_new(sLabel);
        pTabLabel = gtk_label_new(sTabLabel);
        pMenuLabel = gtk_label_new(sMenuLabel);

        /* Insertion de la page */
        gtk_notebook_append_page_menu(GTK_NOTEBOOK(pNotebook), pLabel, pTabLabel, pMenuLabel);

        g_free(sLabel);
        g_free(sTabLabel);
        g_free(sMenuLabel);
    }
    /* Activation du menu popup */
    gtk_notebook_popup_enable(GTK_NOTEBOOK(pNotebook));

    g_signal_connect(G_OBJECT(pButton), "clicked", G_CALLBACK(OnButton), pNotebook);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

void OnButton(GtkWidget *pButton, gpointer data)
{
    GtkWidget *pDialog;
    GtkWidget *pChild;
    gint iPageNum;
    const gchar *sLabel;
    const gchar *sTabLabel;
    const gchar *sMenuLabel;
    gchar *sDialogText;

    /* Recuperation de la page active */
    iPageNum = gtk_notebook_get_current_page(GTK_NOTEBOOK(data));
    /* Recuperation du widget enfant */
    pChild = gtk_notebook_get_nth_page(GTK_NOTEBOOK(data), iPageNum);

    /* Recuperation du label */
    sLabel = gtk_label_get_text(GTK_LABEL(pChild));
    /* Recuperation du label de l'onglet */
    sTabLabel = gtk_notebook_get_tab_label_text(GTK_NOTEBOOK(data), pChild);
    /* Recuperation du label du menu pop-up */
    sMenuLabel = gtk_notebook_get_menu_label_text(GTK_NOTEBOOK(data), pChild);

    /* Creation du label de la boite de dialogue */
    sDialogText = g_strdup_printf("C'est la page %d\n"
        "Le label est \"%s\"\n"
        "Le label de l'onglet est \"%s\"\n"
        "Le label du menu est \"%s\"\n",
        iPageNum,
        sLabel,
        sTabLabel,
        sMenuLabel);

    pDialog = gtk_message_dialog_new(NULL,
        GTK_DIALOG_MODAL,
        GTK_MESSAGE_INFO,
        GTK_BUTTONS_OK,
        sDialogText);

    gtk_dialog_run(GTK_DIALOG(pDialog));

    gtk_widget_destroy(pDialog);

    g_free(sDialogText);
}

    Résultat :

    Image non disponible
    Image non disponible

    XXII-C. En savoir plus

    XXII-C-1. Les signaux

    Prototypes fonctions callback :

    • change-current-page

      • void user_function(GtkNotebook *notebook, gint arg1, gpointer user_data);

    • focus-tab

      • gboolean user_function(GtkNotebook *notebook, GtkNotebookTab arg1, gpointer user_data);

    • move-focus-out

      • void user_function(GtkNotebook *notebook, GtkDirectionType arg1, gpointer user_data);

    • select-page

      • gboolean user_function(GtkNotebook *notebook, gboolean arg1, gpointer user_data);

    • switch-page

      • void user_function(GtkNotebook *notebook, GtkNotebookPage *page, guint page_num, gpointer user_data);

    XXII-C-2. Fonctions non documentées

    void gtk_notebook_reorder_child(GtkNotebook *notebook, GtkWidget *child, gint position);
void gtk_notebook_set_show_border(GtkNotebook *notebook, gboolean show_border);
gboolean gtk_notebook_get_show_border(GtkNotebook *notebook);
void gtk_notebook_query_tab_label_packing(GtkNotebook *notebook, GtkWidget *child, gboolean *expand, gboolean *fill, GtkPackType *pack_type);
void gtk_notebook_set_tab_label_packing(GtkNotebook *notebook, GtkWidget *child, gboolean expand, gboolean fill, GtkPackType pack_type);


Cours GTK 2

Table des matières

  • XXIII. Le widget GtkTreeView
    • XXIII-A. Introduction
    • XXIII-B. Création d'une liste
      • XXIII-B-1. Création
      • XXIII-B-2. Insertion d'éléments.
    • XXIII-C. Création d'un arbre.
      • XXIII-C-1. Création.
      • XXIII-C-2. Insertion d'éléments.
    • XXIII-D. Affichage du widget GtkTreeView.
      • XXIII-D-1. Création de la vue.
      • XXIII-D-2. Création des colonnes.
      • XXIII-D-3. Exemples.
      • XXIII-D-4. Programme exemple 1
      • XXIII-D-5. Programme exemple 2
    • XXIII-E. En savoir plus
      • XXIII-E-1. Les propriétés
      • XXIII-E-2. Les signaux

    • XXIII. Le widget GtkTreeView

    Nous allons étudier dans ce chapitre comment afficher des données sous forme de liste ou d'arbre. Le widget GtkTreeView utilise plusieurs widgets pour permettre l'affichage de données.

    Image non disponible

    XXIII-A. Introduction

    L'utilisation de ce widget se décompose en deux parties distinctes :

    • La création du modèle ;
    • La gestion de l'affichage.

    La création du modèle se fait soit par l'utilisation de GtkListStore pour créer une liste, soit par GtkTreeStore pour la création d'un arbre. C'est deux objets stockent dans un premier temps la structure des données, c'est-à-dire le nombre de colonnes, le type d'affichage (texte, image, case à cocher) et stockent dans un second temps les données qui seront affichées par la suite.

    Une fois le modèle créer, nous pouvons passer à la partie affichage des données. Le widget principal est bien sûr GtkTreeView qui utilise dans un premier temps l'objet GtkTreeViewColumn qui servira à définir chaque colonne du widget. Et enfin pour chaque colonne il faut définir le type d'affichage à l'aide des objets GtkCellRendererText (pour afficher du texte), GtkCellRendererPixbuf (pour afficher une image) et GtkCellRenderToggle (pour afficher une case à cocher).

    Regardons maintenant comment coder cela.

    XXIII-B. Création d'une liste

    XXIII-B-1. Création

    Pour créer un modèle de type liste, nous allons utiliser cette fonction :

    GtkListStore* gtk_list_store_new(gint n_columns, ...);

    Le premier paramètre de cette fonction définie le nombre de colonnes du modèle et donc du GtkTreeView. A la suite de ce paramètre, il faut définir le type de donnée qui sera stockée dans chaque colonne. Les types les plus communs sont :

    • G_TYPE_STRING pour afficher une chaîne de caractère ;
    • G_TYPE_BOOLEAN pour utiliser une case à cocher ;
    • G_TYPE_INT pour afficher un entier ;
    • G_TYPE_FLOAT pour afficher un float ;
    • G_TYPE_DOUBLE pour afficher un double ;
    • GDK_TYPE_PIXBUF pour afficher une image.

    Une fois que les différentes colonnes sont définies, nous allons voir comment ajouter des éléments à la liste.

    XXIII-B-2. Insertion d'éléments.

    La première étape de l'insertion consiste à créer une nouvelle entrée (ligne) à la liste. Pour cela, nous allons utiliser l'objet GtkTreeIter. Cet objet permet de savoir à quel endroit nous nous positionnons dans la liste. La création d'une nouvelle ligne va donner une nouvelle position dont nous avons besoin pour entrer les données.

    Voici la liste des fonctions qui permettent de créer une nouvelle entrée :

    void gtk_list_store_append(GtkListStore *list_store, GtkTreeIter *iter);
void gtk_list_store_prepend(GtkListStore *list_store, GtkTreeIter *iter);
void gtk_list_store_insert(GtkListStore *list_store, GtkTreeIter *iter, gint position);
void gtk_list_store_insert_before(GtkListStore *list_store, GtkTreeIter *iter, GtkTreeIter *sibling);
void gtk_list_store_insert_after(GtkListStore *list_store, GtkTreeIter *iter, GtkTreeIter *sibling);

    Pour chacune de ces fonctions, le premier paramètre list_store est la liste qui a été crée précédemment et le deuxième paramètre iter, est la position de la ligne nouvellement crée.
    Les deux premières fonctions sont les plus couramment utilisées lors de la création de la liste. La première fonction crée une nouvelle ligne à la fin de la liste tandis que la deuxième fonction l'ajoute au début de la liste.
    Les trois fonctions suivantes permettent d'introduire une nouvelle ligne à une position bien précise. La troisième fonction l'ajoute par rapport à une valeur numérique (paramètre position), la quatrième fonction l'ajoute avant une ligne bien définie par son itération sibling et la cinquième fonction l'ajoute après une ligne définie de la même manière.

    Il faut maintenant définir le contenu de la nouvelle ligne, en utilisant cette unique fonction :

    void gtk_list_store_set(GtkListStore *list_store, GtkTreeIter *iter…);

    Les paramètres list_store et iter sont bien sûr la liste et la ligne pour laquelle nous voulons spécifier les données. A la suite de ces deux paramètres il suffit d'ajouter les différentes données avec dans l'ordre la colonne de la donnée suivie de sa valeur. Et pour terminer, il faut dire à cette fonction que la liste des données est terminée simplement en ajoutant -1 en dernier paramètre.

    La liste est maintenant prête à l'affichage.

    XXIII-C. Création d'un arbre.

    Nous allons maintenant voir les légères différences qu'il y a entre la création d'une liste et la création d'un arbre à l'aide de l'objet GtkTreeStore. La principale différence est que chaque ligne peut avoir une ligne parente et une ou plusieurs lignes enfants. Nous n'avons donc plus affaire à une simple succession de lignes, mais à une organisation imbriquée de succession de lignes.

    XXIII-C-1. Création.

    La fonction de création est très ressemblante à celle de l'objet GtkListStore :

    GtkTreeStore* gtk_tree_store_new(gint n_columns, ...);

    Bien entendu, le paramètre n_columns est le nombre de colonne de chaque ligne de l'arbre, suivi des différents types des données.

    XXIII-C-2. Insertion d'éléments.

    Cette fois aussi les fonctions sont presque identiques à celles fournies pour les listes :

    void gtk_tree_store_append(GtkTreeStore *tree_store, GtkTreeIter *iter, GtkTreeIter *parent);
void gtk_tree_store_prepend(GtkTreeStore *tree_store, GtkTreeIter *iter, GtkTreeIter *parent);
void gtk_tree_store_insert(GtkTreeStore *tree_store, GtkTreeIter *iter, GtkTreeIter *parent, gint position);
void gtk_tree_store_insert_before(GtkTreeStore *tree_store, GtkTreeIter *iter, GtkTreeIter *parent, GtkTreeIter *sibling);
void gtk_tree_store_insert_after(GtkTreeStore *tree_store, GtkTreeIter *iter, GtkTreeIter *parent, GtkTreeIter *sibling);

    La grande nouveauté, avec toutes ces fonctions, est la présence d'un paramètre supplémentaire, parent, de type GtkTreeIter, qui n'est autre que la ligne dont notre nouvel élément sera l'enfant.

    Si notre nouvelle ligne n'a pas de parent, ce paramètre sera à NULL, sinon il faudra mettre l'itération correspondant à la ligne parente.

    Et pour finir, il faut définir les valeurs de chaque colonne grâce à cette fonction :

    void gtk_tree_store_set(GtkTreeStore *tree_store, GtkTreeIter *iter, ...);

    De la même façon que pour les éléments de type GtkListStore, la liste de valeurs doit se terminer par -1.

    XXIII-D. Affichage du widget GtkTreeView.

    Maintenant que le modèle du widget GtkTreeView a été créé, il faut gérer l'affichage de celui-ci. Cela va se dérouler en deux étapes distinctes :

    • Création de la vue ;
    • Création des colonnes.

    Le déroulement de ces différentes étapes est identique que cela soit avec un modèle de type GtkListStore qu'avec un modèle de type GtkTreeStore.

    XXIII-D-1. Création de la vue.

    Il existe deux fonctions de création du widget GtkTreeView :

    GtkWidget* gtk_tree_view_new(void);
GtkWidget* gtk_tree_view_new_with_model(GtkTreeModel *model);

    La première fonction crée une vue vide, alors que la deuxième fonction créera la vue à partir du modèle spécifié. Que l'on utilise un GtkListStore ou un GtkTreeStore, il faudra utiliser la macro GTK_TREE_MODEL pour spécifier le paramètre model.

    Si la première fonction est utilisée pour la création de la vue, il faudra tout de même donner à la vue un modèle bien précis, pour que lors de l'affichage GTK+ sache quelles données il doit afficher, avec cette fonction :

    void gtk_tree_view_set_model(GtkTreeView *tree_view, GtkTreeModel *model);

    XXIII-D-2. Création des colonnes.

    Lors de cette étape, nous allons introduire deux nouveaux objets. Tout d'abord l'objet GtkCellRenderer et ses dérivés, puis l'objet GtkTreeViewColumn. Nous allons donc maintenant voir comment ajouter les différentes colonnes à notre vue et aussi de quelle manière doit être fait le rendu.

    La manière dont les cases d'une colonne sont rendues se définit à l'aide des objets dérivés de GtkCellRenderer :

    • GtkCellRendererText pour afficher du texte ;
    • GtkCellRendererToggle pour afficher une case à cocher ou un bouton radio ;
    • GtkCellRendererPixbuf pour afficher une image.

    Pour créer un de ces objets, nous avons les fonctions suivantes :

    GtkCellRenderer* gtk_cell_renderer_text_new(void);
GtkCellRenderer* gtk_cell_renderer_toggle_new(void);
GtkCellRenderer* gtk_cell_renderer_pixbuf_new(void);

    Une fois le GtkCellRenderer créé, nous pouvons modifier différents paramètres à l'aide de cette fonction :

    void g_object_set(gpointer object, const gchar *first_property_name, ...);

    Le premier paramètre, object, est l'objet que l'on veut modifier. Il faut utiliser pour ce paramètre la macro G_OBJECT() . Puis, les paramètres suivants vont par couple paramètre/valeur pour modifier par exemple la couleur de fond de la case ou autre. Une fois tous les paramètres à modifier entrés, il faut ajouter NULL pour dire que la liste des modifications à apporter est terminée.

    La liste des paramètres que nous pouvons modifier pour les objets de types GtkCellRenderer est disponible dans la section « En savoir plus » de ce chapitre.

    Maintenant que nous savons comment les cases d'une colonne doivent être rendues, nous allons voir comment créer une colonne et comment l'ajouter à la vue. Pour créer une colonne, nous avons cette fonction :

    GtkTreeViewColumn* gtk_tree_view_column_new_with_attributes(const gchar *title, GtkCellRenderer *cell, ...);

    Le premier paramètre, title, est le texte qui sera affiché en haut de la colonne dans une ligne bien spécifique. Le second paramètre, cell, est simplement l'objet GtkCellRenderer que nous venons juste de créer, définissant le rendu de chaque case de la colonne.

    Ensuite, il faut à nouveau définir le type de la colonne ainsi que le numéro de la colonne dans le modèle. Les types de colonnes sont « text » pour du texte, « active » pour une case à cocher, « pixbuf » pour une image.

    Et pour terminer, comme souvent dans ce type de fonction, il faut ajouter NULL à la suite de tous les paramètres pour dire que la liste est terminée.

    Il ne nous reste plus qu'à ajouter la colonne à la vue avec l'une de ces fonctions :

    gint gtk_tree_view_append_column(GtkTreeView *tree_view, GtkTreeViewColumn *column);
gint gtk_tree_view_insert_column(GtkTreeView *tree_view, GtkTreeViewColumn *column, gint position);

    La première fonction ajoute la colonne à la suite des autres, tandis que la seconde l'ajoute à la position position ou à la suite des autres si la valeur de position est invalide. La macro GTK_TREE_VIEW() est à utiliser pour le premier paramètre.

    XXIII-D-3. Exemples.

    Nous allons créer deux exemples pour montrer l'utilisation du widget GtkTreeView. Dans le premier exemple, nous allons créer une liste avec simplement une colonne avec du texte et une colonne comportant une case à cocher. Le deuxième exemple, utilisera quant à lui un arbre avec une première colonne affichant une image, et du texte dans la deuxième colonne.

    XXIII-D-4. Programme exemple 1

    #include <stdlib.h>
#include <stdio.h>
#include <gtk/gtk.h>

enum {
    TEXT_COLUMN,
    TOGGLE_COLUMN,
    N_COLUMN
};

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pListView;
    GtkWidget *pScrollbar;
    GtkListStore *pListStore;
    GtkTreeViewColumn *pColumn;
    GtkCellRenderer *pCellRenderer;
    gchar *sTexte;
    gint i;

    gtk_init(&argc, &argv);

    /* Creation de la fenetre principale */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkTreeView et GtkListStore");
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    /* Creation du modele */
    pListStore = gtk_list_store_new(N_COLUMN, G_TYPE_STRING, G_TYPE_BOOLEAN);

    sTexte = g_malloc(12);

    /* Insertion des elements */
    for(i = 0 ; i < 10 ; ++i)
    {
        GtkTreeIter pIter;

        sprintf(sTexte, "Ligne %d\0", i);

        /* Creation de la nouvelle ligne */
        gtk_list_store_append(pListStore, &pIter);

        /* Mise a jour des donnees */
        gtk_list_store_set(pListStore, &pIter,
            TEXT_COLUMN, sTexte,
            TOGGLE_COLUMN, TRUE,
            -1);
    }

    g_free(sTexte);

    /* Creation de la vue */
    pListView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(pListStore));

    /* Creation de la premiere colonne */
    pCellRenderer = gtk_cell_renderer_text_new();
    pColumn = gtk_tree_view_column_new_with_attributes("Titre",
        pCellRenderer,
        "text", TEXT_COLUMN,
        NULL);

    /* Ajout de la colonne à la vue */
    gtk_tree_view_append_column(GTK_TREE_VIEW(pListView), pColumn);

    /* Creation de la deuxieme colonne */
    pCellRenderer = gtk_cell_renderer_toggle_new();
    pColumn = gtk_tree_view_column_new_with_attributes("CheckBox",
        pCellRenderer,
        "active", TOGGLE_COLUMN,
        NULL);

    /* Ajout de la colonne à la vue */
    gtk_tree_view_append_column(GTK_TREE_VIEW(pListView), pColumn);

    /* Ajout de la vue a la fenetre */
    pScrollbar = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(pScrollbar),
        GTK_POLICY_AUTOMATIC,
        GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(pScrollbar), pListView);
    gtk_container_add(GTK_CONTAINER(pWindow), pScrollbar);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible
    Image non disponible

    XXIII-D-5. Programme exemple 2

    #include <stdlib.h>
#include <stdio.h>
#include <gtk/gtk.h>

enum {
    BMP_COLUMN,
    TEXT_COLUMN,
    N_COLUMN
};

/* Utilisateurs Visual C++ : il faut ajouter gdk_pixbuf-2.0.lib dans les options du linker */

int main(int argc, char **argv)
{
    GtkWidget *pWindow;
    GtkWidget *pTreeView;
    GtkWidget *pScrollbar;
    GtkTreeStore *pTreeStore;
    GtkTreeViewColumn *pColumn;
    GtkCellRenderer *pCellRenderer;
    GdkPixbuf *pPixBufA;
    GdkPixbuf *pPixBufB;
    gchar *sTexte;
    gint i;

    gtk_init(&argc, &argv);

    /* Creation de la fenetre principale */
    pWindow = gtk_window_new(GTK_WINDOW_TOPLEVEL);
    gtk_window_set_default_size(GTK_WINDOW(pWindow), 320, 200);
    gtk_window_set_title(GTK_WINDOW(pWindow), "GtkTreeView et GtkTreeStore");
    g_signal_connect(G_OBJECT(pWindow), "destroy", G_CALLBACK(gtk_main_quit), NULL);

    /* Creation du modele */
    pTreeStore = gtk_tree_store_new(N_COLUMN, GDK_TYPE_PIXBUF, G_TYPE_STRING);

    sTexte = g_malloc(16);

    /* Chargement des images */
    pPixBufA = gdk_pixbuf_new_from_file("./icon_computer.png", NULL);
    pPixBufB = gdk_pixbuf_new_from_file("./icon_directory.png", NULL);

    /* Insertion des elements */
    for(i = 0 ; i < 10 ; ++i)
    {
        GtkTreeIter pIter;
        GtkTreeIter pIter2;
        gint j;

        sprintf(sTexte, "Ordinateur %d", i);

        /* Creation de la nouvelle ligne */
        gtk_tree_store_append(pTreeStore, &pIter, NULL);

        /* Mise a jour des donnees */
        gtk_tree_store_set(pTreeStore, &pIter,
            BMP_COLUMN, pPixBufA,
            TEXT_COLUMN, sTexte,
            -1);

        for(j = 0 ; j < 2 ; ++j)
        {
            sprintf(sTexte, "Repertoire %d", j);

            /* Creation de la nouvelle ligne enfant */
            gtk_tree_store_append(pTreeStore, &pIter2, &pIter);

            /* Mise a jour des donnees */
            gtk_tree_store_set(pTreeStore, &pIter2,
                BMP_COLUMN, pPixBufB,
                TEXT_COLUMN, sTexte,
                -1);
        }
    }

    g_free(sTexte);

    g_object_unref(pPixBufA);
    g_object_unref(pPixBufB);

    /* Creation de la vue */
    pTreeView = gtk_tree_view_new_with_model(GTK_TREE_MODEL(pTreeStore));

    /* Creation de la premiere colonne */
    pCellRenderer = gtk_cell_renderer_pixbuf_new();
    pColumn = gtk_tree_view_column_new_with_attributes("Image",
        pCellRenderer,
        "pixbuf", BMP_COLUMN,
        NULL);

    /* Ajout de la colonne à la vue */
    gtk_tree_view_append_column(GTK_TREE_VIEW(pTreeView), pColumn);

    /* Creation de la deuxieme colonne */
    pCellRenderer = gtk_cell_renderer_text_new();
    pColumn = gtk_tree_view_column_new_with_attributes("Label",
        pCellRenderer,
        "text", TEXT_COLUMN,
        NULL);

    /* Ajout de la colonne à la vue */
    gtk_tree_view_append_column(GTK_TREE_VIEW(pTreeView), pColumn);

    /* Ajout de la vue a la fenetre */
    pScrollbar = gtk_scrolled_window_new(NULL, NULL);
    gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(pScrollbar),
        GTK_POLICY_AUTOMATIC,
        GTK_POLICY_AUTOMATIC);
    gtk_container_add(GTK_CONTAINER(pScrollbar), pTreeView);
    gtk_container_add(GTK_CONTAINER(pWindow), pScrollbar);

    gtk_widget_show_all(pWindow);

    gtk_main();

    return EXIT_SUCCESS;
}

    Résultat :

    Image non disponible
    Image non disponible

    XXIII-E. En savoir plus

    XXIII-E-1. Les propriétés

    GtkTreeView

    Propriété | Type | Accès
enable-search | gboolean | Lecture/Ecriture
expander-column | GtkTreeViewColumn | Lecture/Ecriture
hadjustment | GtkAdjustment | Lecture/Ecriture
headers-clickable | gboolean | Ecriture
headers-visible | gboolean | Lecture/Ecriture
model | GtkTreeMode | Lecture/Ecriture
reorderable | gboolean | Lecture/Ecriture
rules-hint | gboolean | Lecture/Ecriture
search-column | gint | Lecture/Ecriture
vadjustment | GtkAdjustment | Lecture/Ecriture
allow-rules | gboolean | Lecture
even-row-color | GdkColor | Lecture
expander-size | gint | Lecture
horizontal-separator | gint | Lecture
indent-expanders | gboolean | Lecture
odd-row-color | GdkColor | Lecture
vertical-separator | gint | Lecture

    GtkTreeViewColumn

    Propriété | Type | Accès
alignment | gfloat | Lecture/Ecriture
clickable | gboolean | Lecture/Ecriture
fixed-width | gint | Lecture/Ecriture
max-width | gint | Lecture/Ecriture
min-width | gint | Lecture/Ecriture
reorderable | gboolean | Lecture/Ecriture
resizable | gboolean | Lecture/Ecriture
sizing | GtkTreeViewColumnSizing | Lecture/Ecriture
sort-indicator | gboolean | Lecture/Ecriture
sort-order | GtkSortType | Lecture/Ecriture
title | gchararray | Lecture/Ecriture
visible | gboolean | Lecture/Ecriture
widget | GtkWidget | Lecture/Ecriture
width | gint | Lecture

    GtkCellRenderer

    Propriété | Type | Accès
cell-background | gchararray | Ecriture
cell-background-gdk | GdkColor | Lecture/Ecriture
cell-background-set | gboolean | Lecture/Ecriture
height | gint | Lecture/Ecriture
is-expanded | gboolean | Lecture/Ecriture
is-expander | gboolean | Lecture/Ecriture
mode | GtkCellRendererMode | Lecture/Ecriture
visible | gboolean | Lecture/Ecriture
width | gint | Lecture/Ecriture
xalign | gfloat | Lecture/Ecriture
xpad | guint | Lecture/Ecriture
yalign | gfloat | Lecture/Ecriture
ypad | guint | Lecture/Ecriture

    GtkCellRenderText

    Propriété | Type | Accès
attributes | PangoAttrList | Lecture/Ecriture
background | gchararray | Ecriture
background-gdk | GdkColor | Lecture/Ecriture
background-set | gboolean | Lecture/Ecriture
editable | gboolean | Lecture/Ecriture
editable-set | gboolean | Lecture/Ecriture
family | gchararray | Lecture/Ecriture
family-set | gboolean | Lecture/Ecriture
font | gchararray | Lecture/Ecriture
font-desc | PangoFontDescription | Lecture/Ecriture
foreground | gchararray | Ecriture
foreground-gdk | GdkColor | Lecture/Ecriture
foreground-set | gboolean | Lecture/Ecriture
markup | gchararray | Ecriture
rise | gint | Lecture/Ecriture
rise-set | gboolean | Lecture/Ecriture
scale | gdouble | Lecture/Ecriture
scale-set | gboolean | Lecture/Ecriture
size | gint | Lecture/Ecriture
size-points | gdouble | Lecture/Ecriture
size-set | gboolean | Lecture/Ecriture
stretch | PangoStretch | Lecture/Ecriture
stretch-set | gboolean | Lecture/Ecriture
strikethrough | gboolean | Lecture/Ecriture
strikethrough-set | gboolean | Lecture/Ecriture
style | PangoStyle | Lecture/Ecriture
style-set | gboolean | Lecture/Ecriture
text | gchararray | Lecture/Ecriture
underline | PangoUnderline | Lecture/Ecriture
underline-set | gboolean | Lecture/Ecriture
variant | PangoVariant | Lecture/Ecriture
variant-set | gboolean | Lecture/Ecriture
weight | gint | Lecture/Ecriture
weight-set | gboolean | Lecture/Ecriture

    GtkCellRendererToggle

    Propriété | Type | Accès
activatable | gboolean | Lecture/Ecriture
active | gboolean | Lecture/Ecriture
inconsistent | gboolean | Lecture/Ecriture
radio | gboolean | Lecture/Ecriture

    GtkCellRenderPixbuf

    Propriété | Type | Accès
pixbuf | GdkPixbuf | Lecture/Ecriture
pixbuf-expander-closed | GdkPixbuf | Lecture/Ecriture
pixbuf-expander-open | GdkPixbuf | Lecture/Ecriture
stock-detail | gchararray | Lecture/Ecriture
stock-id | gchararray | Lecture/Ecriture
stock-size | GtkIconSize | Lecture/Ecriture

    XXIII-E-2. Les signaux

    Prototypes fonctions callback :

    GtkTreeView

    • columns-changed

      • void user_function(GtkTreeView *treeview, gpointer user_data);

    • cursor-changed

      • void user_function(GtkTreeView *treeview, gpointer user_data);

    • expand-collapse-cursor-row

      • gboolean user_function(GtkTreeView *treeview, gboolean arg1, gboolean arg2, gboolean arg3, gpointer user_data);

    • move-cursor

      • gboolean user_function(GtkTreeView *treeview, GtkMovementStep arg1, gint arg2, gpointer user_data);

    • row-activated

      • void user_function(GtkTreeView *treeview, GtkTreePath *arg1, GtkTreeViewColumn *arg2, gpointer user_data);

    • row-collapsed

      • void user_function(GtkTreeView *treeview, GtkTreeIter *arg1, GtkTreePath *arg2, gpointer user_data);

    • row-expanded

      • void user_function(GtkTreeView *treeview, GtkTreeIter *arg1, GtkTreePath *arg2, gpointer user_data);

    • select-all

      • gboolean user_function(GtkTreeView *treeview, gpointer user_data);

    • select-cursor-parent

      • gboolean user_function(GtkTreeView *treeview, gpointer user_data);

    • select-cursor-row

      • gboolean user_function(GtkTreeView *treeview, gboolean arg1, gpointer user_data);

    • set-scroll-adjustments

      • void user_function(GtkTreeView *treeview, GtkAdjustment *arg1, GtkAdjustment *arg2, gpointer user_data);

    • start-interactive-search

      • gboolean user_function(GtkTreeView *treeview, gpointer user_data);

    • test-collapse-row

      • gboolean user_function(GtkTreeView *treeview, GtkTreeIter *arg1, GtkTreePath *arg2, gpointer user_data);

    • test-expand-row

      • gboolean user_function(GtkTreeView *treeview, GtkTreeIter *arg1, GtkTreePath *arg2, gpointer user_data);

    • toggle-cursor-row

      • gboolean user_function(GtkTreeView *treeview, gpointer user_data);

    • unselect-all

      • gboolean user_function(GtkTreeView *treeview, gpointer user_data);

    GtkTreeViewColumn

    • clicked

      • void user_function(GtkTreeViewColumn *treeviewcolumn, gpointer user_data);

    GtkCellRendererText

    • edited

      • void user_function(GtkCellRendererText *cellrenderertext, gchar *arg1, gchar *arg2, gpointer user_data);

    GtkCellRendererToggle

    • toggled

      • void user_function(GtkCellRendererToggle *cellrenderertoggle, gchar *arg1, gpointer user_data);


Cours GTK 2

Table des matières

  • XXV. Licence
    • XXV-A. PREAMBLE
    • XXV-B. APPLICABILITY AND DEFINITIONS
    • XXV-C. VERBATIM COPYING
    • XXV-D. COPYING IN QUANTITY
    • XXV-E. MODIFICATIONS
    • XXV-F. COMBINING DOCUMENTS
    • XXV-G. COLLECTIONS OF DOCUMENTS
    • XXV-H. AGGREGATION WITH INDEPENDENT WORKS
    • XXV-I. TRANSLATION
    • XXV-J. TERMINATION
    • XXV-K. FUTURE REVISIONS OF THIS LICENSE

    • XXV. Licence

    GNU Free Documentation License

    Version 1.1, March 2000

    Copyright (C) 2000 Free Software Foundation, Inc.

    59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    Everyone is permitted to copy and distribute verbatim copies

    of this license document, but changing it is not allowed.

    XXV-A. PREAMBLE

    The purpose of this License is to make a manual, textbook, or other written document « free » in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others.

    This License is a kind of « copyleft », which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software.

    We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

    XXV-B. APPLICABILITY AND DEFINITIONS

    This License applies to any manual or other work that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. The « Document », below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as « you ».

    A « Modified Version » of the Document means any work containing the Document or a portion of it, either copied verbatim, or with modifications and/or translated into another language.

    A « Secondary Section » is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Document's overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (For example, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them.

    The « Invariant Sections » are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License.

    The « Cover Texts » are certain short passages of text that are listed, as Front-Cover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License.

    A « Transparent » copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, whose contents can be viewed and edited directly and straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup has been designed to thwart or discourage subsequent modification by readers is not Transparent. A copy that is not « Transparent » is called « Opaque ».

    Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML designed for human modification. Opaque formats include PostScript, PDF, proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML produced by some word processors for output purposes only.

    The « Title Page » means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, « Title Page » means the text near the most prominent appearance of the work's title, preceding the beginning of the body of the text.

    XXV-C. VERBATIM COPYING

    You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3.

    You may also lend copies, under the same conditions stated above, and you may publicly display copies.

    XXV-D. COPYING IN QUANTITY

    If you publish printed copies of the Document numbering more than 100, and the Document's license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects.

    If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages.

    If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a publicly-accessible computer-network location containing a complete Transparent copy of the Document, free of added material, which the general network-using public has access to download anonymously at no charge using public-standard network protocols. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

    It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

    XXV-E. MODIFICATIONS

    You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version:

    A. Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission.

    B. List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has less than five).

    C. State on the Title page the name of the publisher of the Modified Version, as the publisher.

    D. Preserve all the copyright notices of the Document.

    E. Add an appropriate copyright notice for your modifications adjacent to the other copyright notices.

    F. Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below.

    G. Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document's license notice.

    H. Include an unaltered copy of this License.

    I. Preserve the section entitled « History », and its title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section entitled « History » in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence.

    J. Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the « History » section. You may omit a network location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission.

    K. In any section entitled « Acknowledgements » or « Dedications », preserve the section's title, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein.

    L. Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles.

    M. Delete any section entitled « Endorsements ». Such a section may not be included in the Modified Version.

    N. Do not retitle any existing section as « Endorsements » or to conflict in title with any Invariant Section.

    If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version's license notice. These titles must be distinct from any other section titles.

    You may add a section entitled « Endorsements », provided it contains nothing but endorsements of your Modified Version by various parties--for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard.

    You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one.

    The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

    XXV-F. COMBINING DOCUMENTS

    You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice.

    The combined work need only contain one copy of this License, and multiple identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work.

    In the combination, you must combine any sections entitled « History » in the various original documents, forming one section entitled « History »; likewise combine any sections entitled « Acknowledgements », and any sections entitled « Dedications ». You must delete all sections entitled « Endorsements. »

    XXV-G. COLLECTIONS OF DOCUMENTS

    You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects.

    You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

    XXV-H. AGGREGATION WITH INDEPENDENT WORKS

    A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, does not as a whole count as a Modified Version of the Document, provided no compilation copyright is claimed for the compilation. Such a compilation is called an « aggregate », and this License does not apply to the other self-contained works thus compiled with the Document, on account of their being thus compiled, if they are not themselves derivative works of the Document.

    If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one quarter of the entire aggregate, the Document's Cover Texts may be placed on covers that surround only the Document within the aggregate. Otherwise they must appear on covers around the whole aggregate.

    XXV-I. TRANSLATION

    Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation of this License provided that you also include the original English version of this License. In case of a disagreement between the translation and the original English version of this License, the original English version will prevail.

    XXV-J. TERMINATION

    You may not copy, modify, sublicense, or distribute the Document except as expressly provided for under this License. Any other attempt to copy, modify, sublicense or distribute the Document is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.

    XXV-K. FUTURE REVISIONS OF THIS LICENSE

    The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/.

    Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License « or any later version » applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation.


Cours GTK 2

XXVI. Historique

19 décembre 2003 - Version 1.4

Nouveaux chapitres :

  • Les tables
  • Les pages à onglets
  • Le widget GtkTreeView

21 mai 2003 - Version 1.2

Nouveaux chapitres :

  • La barre d'outils
  • La sélection de valeurs numériques
  • La barre de progression

Mise à jour des chapitres :

  • Les labels
  • Les décorations
  • Les images
  • Les boîtes de dialogue
  • La barre d'état

8 février 2003 - Version 1.1.2

Mise à jour des chapitres :

  • Les labels
  • Les boutons (Partie 1)
  • Les box
  • Les entrées de saisie
  • Les boîtes de dialogue

Ajout d'un chapitre :

  • Les décorations

25 janvier 2003 - Version 1.1.1

Corrections de fautes d'orthographes.

17 novembre 2002 - Version 1.1

Mise à jour des chapitres :

  • Introduction
  • Premier programme
  • Les fenêtre

Ajout des chapitres

  • Sélection des fichiers
  • Les fenêtres avec barres de défilement
  • Les zones de texte
  • La barre d'état
  • Les images

23 septembre 2002 - Version 1.0

Contenu du cours :

  • Présentation
  • Premier programme
  • Les fenêtres
  • Les labels
  • Les boutons (partie 1)
  • Les box
  • Les listes chaînées
  • Les entrées de saisie
  • Les boîtes de dialogue
  • Les menus
  • Les boutons (partie 2)


Vu dans ... http://nicolasj.developpez.com/gtk/cours

GTK+ par l'exemple

Table des matières

  • Page 1
    • I. Introduction
      • I-A. But de ce tutoriel
      • I-B. Connaissances requises
      • I-C. Quelques mots sur GTK+
        • I-C-1. Historique
        • I-C-2. Structure
        • I-C-3. Pourquoi utiliser GTK+ ?
      • I-D. Installation
        • I-D-1. Windows
        • I-D-2. Linux
      • I-E. La philosophie GTK+
      • I-F. Quelques notions de POO
      • I-G. Notre premier programme
      • I-H. Code source
  • Page 2
    • II. Notre première fenêtre
      • II-A. Aperçu
      • II-B. Création
      • II-C. Affichage
      • II-D. Destruction
      • II-E. Code source
  • Page 3
    • III. Fermer notre fenêtre grâce aux boutons
      • III-A. Aperçu
      • III-B. Les boutons poussoir
      • III-C. Code source
  • Page 4
    • IV. Comment afficher plusieurs widgets
      • IV-A. Aperçu
      • IV-B. Le problème
      • IV-C. La solutions
      • IV-D. Code source
  • Page 5
    • V. Afficher le contenue d'un fichier
      • V-A. Aperçu
      • V-B. Saisir du texte
      • V-C. Ouvrir un fichier
      • V-D. La petite touche finale
      • V-E. Code source
  • Page 6
    • VI. Choisir un fichier
      • VI-A. Aperçu
      • VI-B. Utilisation d'un GtkFileChooserFile
      • VI-C. Code source
  • Page 7
    • VII. Interlude
      • VII-A. Code source
  • Page 8
    • VIII. Sauvegarder les modification
      • VIII-A. Aperçu
      • VIII-B. Enregistrer
      • VIII-C. Enregistrer sous
      • VIII-D. Code source
  • Page 9
    • IX. Créer un nouveau document
      • IX-A. Aperçu
      • IX-B. Nouveau fichier
      • IX-C. Code source
  • Page 10
    • X. Fermer
      • X-A. Aperçu
      • X-B. Fermer un fichier
      • X-C. Enregistrer avant de fermer
      • X-D. Code source
  • Page 11
    • XI. Les barres de défilement
      • XI-A. Aperçu
      • XI-B. Ajouter des barres de défilement
      • XI-C. Code source
  • Page 12
    • XII. Les menus
      • XII-A. Aperçu
      • XII-B. Création du menu
      • XII-C. Code source
  • Page 13
    • XIII. Les barres d'outils
      • XIII-A. Aperçu
      • XIII-B. Création d'une barre d'outils
      • XIII-C. Code source
  • Page 14
    • XIV. Les racourcis clavier
      • XIV-A. Aperçu
      • XIV-B. Mise en place des raccourcis clavier
      • XIV-C. Code source
  • Page 15
    • XV. Messages d'erreur
      • XV-A. Aperçu
      • XV-B. Amélioration de nos fonctions d'affichage d'erreur
      • XV-C. Code source
  • Page 16
    • XVI. Ouvrir plusieurs fichiers en même temps
      • XVI-A. Aperçu
      • XVI-B. Mise en place des onglets
      • XVI-C. Changement de page
      • XVI-D. Fermer un onglet
      • XVI-E. Fermer tous les onglets
      • XVI-F. Modifier le titre de la page
      • XVI-G. Code source
  • Page 17
    • XVII. Afficher l'arborescence du disque
      • XVII-A. Aperçu
      • XVII-B. Préparons le terrain
      • XVII-C. Création d'un GtkTreeView
        • XVII-C-1. Création du magasin
        • XVII-C-2. Affichage de l'arborescence
        • XVII-C-3. Sélectionner un fichier
      • XVII-D. Code source
  • Page 18
    • XVIII. Notre signature
      • XVIII-A. Aperçu
      • XVIII-B. Boîte A propos
      • XVIII-C. Code source
  • Page 19
    • XIX. Conclusion
      • XIX-A. C'est déjà fini ?
      • XIX-B. Remerciements

Ce tutoriel a pour but de vous guider dans la réalisation d'une interface graphique en C grâce à GTK+ au travers l'exemple de la réalisation d'un éditeur de texte.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2006-2008 Nicolas Joseph. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.


GTK+ par l'exemple

Table des matières

  • I. Introduction
    • I-A. But de ce tutoriel
    • I-B. Connaissances requises
    • I-C. Quelques mots sur GTK+
      • I-C-1. Historique
      • I-C-2. Structure
      • I-C-3. Pourquoi utiliser GTK+ ?
    • I-D. Installation
      • I-D-1. Windows
      • I-D-2. Linux
    • I-E. La philosophie GTK+
    • I-F. Quelques notions de POO
    • I-G. Notre premier programme

I. Introduction

I-A. But de ce tutoriel

Pour vous initier aux joies de la programmation d'interfaces graphiques, plutôt que de faire un catalogue de toutes les fonctions de la bibliothèque, ce qui serait long et vite ennuyeux, je vous propose de construire pas à pas une application : un éditeur de texte. Pas un simple éditeur de texte, histoire de faire le tour des possibilités de GTK+, nous allons y ajouter pas à pas les fonctionnalités suivantes :

  • Création, ouverture, enregistrement de fichiers textes
  • Possibilité d'ouvrir plusieurs fichiers grâce à une navigation par onglets
  • Ouverture rapide d'un fichier grâce à une liste de fichiers.

Rien d'impressionnant par rapport à un traitement de texte mais plus évolué et plus pratique qu'un simple éditeur de texte. Ce tutoriel est organisé de façon à ajouter une nouvelle fonctionnalité à chaque étape tout en apprenant à maîtriser un ou plusieurs éléments de GTK+.

Comme je viens de le dire, il n'est pas question de faire un catalogue de la bibliothèque, cependant avant d'utiliser un nouveaux type de widget, j'en ferai une brève description qui pourra vous servir de pense bête lors de vos futurs développements. Cette description comportera un aperçu pour les widgets graphiques, sa hiérarchie d'héritage, ses fonctions de créations (constructeurs), les fonctions les plus utilisées et les signaux importants de la classe.

I-B. Connaissances requises

Le but de ce tutorial est d'apprendre à maîtriser GTK+ en partant de zéro, ou presque, en effet avant de vouloir créer une interface graphique, il vaut mieux bien connaître le langage C en mode console. Pour cela je vous renvoie au cours de la rubrique C.

I-C. Quelques mots sur GTK+

I-C-1. Historique

GTK+ ou the GIMP Toolkit était à l'origine une boîte à outils pour les développeurs du logiciel the GIMP (the GNU Image Manipulation Program), qui comme son nom l'indique est un logiciel de manipulation d'images rattaché au projet GNU. Au vu de l'importance de cette boîte à outils, GTK+ a été détaché de the GIMP en septembre 1997. Depuis il existe deux versions : GTK+ 1.0 et GTK+ 2.0, versions qui ne sont pas totalement compatibles cependant la première est encore utilisée dans certaines applications, tel que dans le domaine de l'embarqué du fait de sa complexité moindre. Il est bien évident que c'est la seconde version qui est la plus utilisée, elle est à l'origine de nombreux projets tel que le gestionnaire de fenêtre GNOME (GNU Network Object Model Environment). La dernière version en date est la 2.8.1 annoncée le 24 Août 2005.

I-C-2. Structure

Comme précisé précédemment, GTK+ est une boîte à outils et, en tant que tel, elle est constituée de plusieurs bibliothèques indépendantes développées par l'équipe de GTK+ :

  • La GLib propose un ensemble de fonctions qui couvrent des domaines aussi vastes que les structures de données, la gestion des threads et des processus de façon portable ou encore un analyseur syntaxique pour les fichiers XML et bien d'autre
  • Pango : il s'agit de la bibliothèque d'affichage et de rendue de textes
  • ATK.

La bibliothèque GTK+ en elle-même utilise une approche orientée objet et repose sur plusieurs couches :

  • GObject : il s'agit de la base de l'implémentation des objets pour la POO
  • GDK : bibliothèque graphique de bas niveau
  • GTK+ : la bibliothèque GTK+ elle-même basée sur l'utilisation de widgets

C'est bien sûr cette dernière qui nous intéresse ici, mais la glib nous sera aussi d'une grande aide.

I-C-3. Pourquoi utiliser GTK+ ?

Effectivement, pourquoi choisir d'utiliser GTK+ plutôt qu'une autre bibliothèque pour réaliser une interface graphique. Voici quelques arguments :

  • GTK+ est sous licence libre LGPL, vous pouvez donc l'utiliser pour développer des programmes libres ou commerciaux
  • La portabilité : GTK+ est disponible sur un grand nombre de systèmes dont Windows, Linux, Unix et MacOSX
  • GTK+ est développée en C et pour le langage C, cependant elle est disponible pour d'autres langages tels que Ada, C++ (grâce à gtkmm), Java, Perl, PHP, Python, Ruby ou plus récemment C#.

I-D. Installation

I-D-1. Windows

Pour pouvoir exécuter une application utilisant GTK+, il faut commencer par installer les binaires. Pour faciliter leur installation, il existe un installeur :

Si vous utilisez the Gimp, utilisez les runtimes proposées sur le site http://www.gimp-win.org/.

Pour développer une application, il faut les bibliothèques ainsi que les fichiers d'en-tête disponibles sur gtk.org. Pour ceux qui utilisent Code::Blocks, voici la méthode à suivre : Installation de Gtk+ sous Code::Blocks

I-D-2. Linux

Sous Linux, vous disposez sûrement d'un système de paquets. Il vous faudra installer deux paquets, le premier contenant les fichiers permettant d'exécuter des programmes utilisant GTK+, le second paquet contient les fichiers nécessaires au développement.
Une fois l'installation réussie, compilez à l'aide de la commande :

gcc -Werror -Wall -W -O2 -ansi -pedantic `pkg-config --cflags --libs gtk+-2.0` *.c

Tous les codes de ce tutoriel ont été testés sous Linux Debian et Windows XP avec Code::Blocks.

I-E. La philosophie GTK+

Voici quelques choix qui ont été faits par l'équipe de développement de GTK+ :

  • Les noms de fonctions sont de la forme gtk_type_du_widget_action, ceci rend les noms des fonctions très explicites, en contre partie ils peuvent être très longs
  • Tous les objets manipulés sont des GtkWidget, pour que GTK+ vérifie qu'il s'agit du bon type d'objet lors de l'appel à une fonction, chaque type de widget possède une macro de la forme GTK_TYPE_DU_WIDGET à utiliser pour transtyper vos widgets
  • La gestion des événements qui modifient le comportement de l'application (clique de souris, pression d'une touche du clavier...) se fait grâce à l'utilisation de fonctions de rappel (callback). Pour cela, on connecte les widgets à des fonctions qui seront appelées lorsque l'événement survient.

I-F. Quelques notions de POO

La POO ou Programmation Orientée Objet est un mode de programmation basée sur des objets. GTK+ utilisant ce principe (2), je vais vous en expliquer les principes de bases.
Plutôt que le déroulement du programme soit régie par une suite d'instruction, en POO il est régi par la création, l'interaction et la destruction d'objets, en résumé la vie des objets qui le composent. Un objets est une entité (le parallèle avec la vie courante est simple, un objet pourrait être un ordinateur), en C on pourrait se représenter un objet comme une structure de données.
Un objet est composé de propriétés (ce qui correspond à nos variables) et de méthodes (des fonctions). Pour créer un objet, on fait appel à son constructeur , il s'agit d'une fonction qui a pour but de créer et d'initialiser l'objet. A la fin de sa vie, l'objet doit être détruit, c'est le but du destructeur .
Dans la vie courante, on peut créer des objets complexes à partir de briques de base, il est possible de créer de nouveaux objets en se basant sur un ou plusieurs objets : ce mécanisme est appeler héritage (on parle d'héritage multiple lorsqu'il met en jeu plusieurs objets parents). L'objet ainsi obtenu hérite des propriétés et des méthodes de ses parents tout en pouvant modifier leur comportement et créer ses propres propriétés et méthodes.
Lorsque l'on souhaite créer un patron pour de futures classes, plutôt que de créer une classe de base qui ne sera jamais instanciée, il est possible de créer une interface .
Le dernier concept de POO qui va nous servir dans ce tutoriel est le polymorphisme . Ceci permet à un objet (prenons par exemple camion), de se comporter comme l'un de ses parents (dans le cas du camion, il peut être vu comme un véhicule, au même titre qu'une voiture). Ceci est aussi valable pour des classes implémentant la même interface.
Pour vous initier aux joies de la programmation orientée objet en C, je vous conseille le tutoriel de CGI : POO en C

I-G. Notre premier programme

Un programme qui utilise GTK+ est un programme écrit en C avant tout, il contient donc le code de base de tout programme C :

main.c
#include <stdlib.h>
int main (int argc, char **argv)
{
  /* ... */
  return EXIT_SUCCESS;
}

Pour pouvoir utiliser les fonctions de GTK+, il faut bien sûr inclure le fichier d'en-tête correspondant :

#include <gtk/gtk.h>

La première chose à faire est d'initialiser la machinerie GTK+ grâce à la fonction gtk_init :

void gtk_init (int *argc, char ***argv);

Cette fonction reçoit les arguments passés en ligne de commande, ceux qui sont spécifiques à GTK+ sont ensuite retirés de la liste; d'où l'utilisation des pointeurs.
Ensuite, il nous faut créer tous les widgets dont nous avons besoin, si nécessaire modifier leurs paramètres par défaut, les connecter à des fonctions callback et ensuite demander leur affichage (tout ceci sera explicité dans la suite du tutoriel). Une fois la fenêtre principale créée, il suffit de lancer la boucle principale de GTK+ :

void gtk_main (void);

Cette fonction est une boucle sans fin (seule la fonction gtk_main_quit permet d'en sortir) qui se charge de gérer le déroulement de notre programme (affichage des widgets, envoi de signaux...).
Voici donc notre premier programme en C/GTK+ qui ne fait rien :

main.c
#include <stdlib.h>
#include <gtk/gtk.h>
int main (int argc, char **argv)
{
  /* Initialisation de GTK+ */
  gtk_init (&argc, &argv);
  /* Creation de la fenetre principale de notre application */
  /* Lancement de la boucle principale */
  gtk_main();
  return EXIT_SUCCESS;
}

En plus de ne rien faire notre programme ne peut être terminé que de façon brutale (Ctrl+C) puisque nous n'appelons pas la fonction gtk_main_quit.


GTK+ par l'exemple

Table des matières

  • II. Notre première fenêtre
    • II-A. Aperçu
    • II-B. Création
    • II-C. Affichage
    • II-D. Destruction
    • II-E. Code source

II. Notre première fenêtre

II-A. Aperçu

II-B. Création

La fenêtre principale est représentée par la classe GtkWindow.
La création d'une fenêtre se fait simplement en appelant le constructeur de cette classe, la fonction gtk_window_new :

GtkWidget *gtk_window_new (GtkWindowType type);

Le seul paramètre de cette fonction est le type de fenêtre souhaité. Il n'y a que deux possibilités :

  • GTK_WINDOW_TOPLEVEL : une fenêtre classique (c'est la base de notre application)
  • GTK_WINDOW_POPUP : il s'agit d'une fenêtre avec seulement l'espace de travail (pas de bordure ni de menu système).

Cette fonction renvoie un pointeur sur une structure de type GtkWindow (transformer en GtkWidget grâce au polymorphisme) qui est l'entité qui va nous servir à manipuler notre fenêtre.

II-C. Affichage

Si vous essayez d'écrire un code maintenant, vous ne verrez rien, pourquoi ? Tout simplement parce qu'il faut préciser à GTK+ qu'il faut rendre notre fenêtre visible grâce à la fonction gtk_widget_show :

void gtk_widget_show (GtkWidget *widget);

Voici le code qui permet d'afficher notre première fenêtre :

main.c
#include <stdlib.h>
#include <gtk/gtk.h>
int main (int argc, char **argv)
{
  GtkWidget *p_window = NULL;
  /* Initialisation de GTK+ */
  gtk_init (&argc, &argv);
  /* Creation de la fenetre principale de notre application */
  p_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  /* Affichage de la fenetre principale */
  gtk_widget_show (p_window);
  /* Lancement de la boucle principale */
  gtk_main ();
  return EXIT_SUCCESS;
}

Ceux qui se sont essayés à la création d'applications graphiques avec l' API Windows comprendront la simplicité de ce code.

II-D. Destruction

Dans l'exemple précédent, si vous avez essayé de terminer l'application en fermant la fenêtre (Alt+F4 ou en cliquant sur la petite croix), vous vous êtes peut-être aperçu que l'application tournait encore en fond, c'est le même problème que pour le chapitre précédent : on ne fait pas appel à gtk_main_quit. Mais comment appeler cette fonction puisque qu'une fois gtk_main appelée nous ne pouvons rien faire ? C'est là qu'intervient le mécanisme des callback. A chaque événement qui se produit, la bibliothèque GObject produit un signal, si nous souhaitons modifier le comportement par défaut du signal, il suffit de connecter notre fonction callback à l'événement souhaité :

#define g_signal_connect(instance, detailed_signal, c_handler, data);

Cette macro permet d'intercepter l'événement detailed_signal de l'objet instance grâce à la fonction c_handler qui doit être du type GCallback :

void (*GCallback) (void);

Les fonctions callback peuvent bien sûr recevoir des paramètres mais leur nature et leur nombre dépendent du contexte, tout est géré par la bibliothèque GObject. Le prototype le plus courant pour une fonction de rappel est le suivant :

void callback (GtkWidget *p_widget, gpointer *user_data);

Dont le premier paramètre est le widget qui a reçu le signal et le second correspond au paramètre data de la fonction g_signal_connect, qui nous permet de passer des informations aux fonctions callback. Pour finir proprement notre application, il suffit d'intercepter le signal destroy de notre fenêtre et d'y assigner la fonction gtk_main_quit qui ne prend pas d'argument. Voici donc notre première application fonctionnelle :

main.c
#include <stdlib.h>
#include <gtk/gtk.h>
int main (int argc, char **argv)
{
  GtkWidget *p_window = NULL;
  /* Initialisation de GTK+ */
  gtk_init (&argc, &argv);
  /* Creation de la fenetre principale de notre application */
  p_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  g_signal_connect (G_OBJECT (p_window), "destroy", G_CALLBACK (gtk_main_quit), NULL);
  /* Affichage de la fenetre principale */
  gtk_widget_show (p_window);
  /* Lancement de la boucle principale */
  gtk_main ();
  return EXIT_SUCCESS;
}

Il est plus courant de créer notre propre fonction de rappel pour quitter le programme; ceci permet de libérer la mémoire allouée, fermer les fichiers ouverts...

main.c
#include <stdlib.h>
#include <gtk/gtk.h>
void cb_quit (GtkWidget *, gpointer);
int main (int argc, char **argv)
{
  GtkWidget *p_window = NULL;
  /* Initialisation de GTK+ */
  gtk_init (&argc, &argv);
  /* Creation de la fenetre principale de notre application */
  p_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
   g_signal_connect (G_OBJECT (p_window), "destroy", G_CALLBACK (cb_quit), NULL);
  /* Affichage de la fenetre principale */
  gtk_widget_show (p_window);
  /* Lancement de la boucle principale */
  gtk_main ();
  return EXIT_SUCCESS;
}
void cb_quit (GtkWidget *p_widget, gpointer user_data)
{
  gtk_main_quit();
  /* Parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

Le préfixe cb_ permet de différencier les fonctions callback, qu'il est préférable de regrouper dans un ou plusieurs fichiers séparés.

A la fin de la fonction, je transtype les paramètres inutilisés pour éviter que mon compilateur m'indique des variables non utilisées, c'est le problème avec les fonctions callback, l'API nous impose un prototype.


GTK+ par l'exemple

Table des matières

  • III. Fermer notre fenêtre grâce aux boutons
    • III-A. Aperçu
    • III-B. Les boutons poussoir

III. Fermer notre fenêtre grâce aux boutons

III-A. Aperçu

III-B. Les boutons poussoir

Maintenant que notre programme se termine proprement, il est plus convivial de proposer à l'utilisateur un bouton pour quitter. Pour créer un bouton poussoir, il existe plusieurs possibilités :

GtkWidget *gtk_button_new (void);
GtkWidget *gtk_button_new_with_label (const gchar *label);
GtkWidget *gtk_button_new_with_mnemonic (const gchar *label);
GtkWidget *gtk_button_new_from_stock (const gchar *stock_id);

La première fonction crée un bouton qui peut servir pour contenir n'importe quel autre widget (label, image...). Si vous souhaitez créer un bouton avec du texte dessus, utilisez directement la seconde fonction qui prend en argument le texte à afficher. Si vous souhaitez ajouter un raccourci clavier (accessible avec la touche Alt), la troisième fonction permet d'en spécifier un en faisant précéder une lettre (généralement la première) d'un underscore '_' (si vous souhaitez afficher le caractère '_', il faut en mettre deux). Enfin la dernière fonction permet d'utiliser les Stock Items qui sont un ensemble d'éléments prédéfinis par GTK+ pour les menus et barres d'outils. Le seul paramètre de cette fonction est le nom de l'élément et GTK+ se charge d'afficher l'icône appropriée ainsi que le texte suivant la langue choisie et un raccourci.
C'est bien sûr cette dernière fonction que nous allons utiliser et ce le plus souvent possible. Voici le code pour créer un tel bouton :

main.c
GtkWidget *p_button = NULL;
p_button = gtk_button_new_from_stock (GTK_STOCK_QUIT);

Pour obtenir tous les types de stocks items disponibles : GtkStockItem
Bien sûr, comme pour la fenêtre si l'on veut voir quelque chose, il faut demander à GTK+ d'afficher notre widget :

main.c
gtk_widget_show (p_button);

Mais pas seulement ! En effet, il faut préciser à GTK+ où doit être affiché notre bouton. Pour cela, la classe GtkWindow est dérivée de la classe GtkContainer qui est, comme son nom le laisse penser, un conteneur pour widget. Pour ajouter un widget, il suffit de faire appel à la fonction gtk_container_add :

void gtk_container_add (GtkContainer *container, GtkWidget *widget);

Le premier paramètre de cette fonction est un GtkContainer, or nous souhaitons passer notre fenêtre qui est un GtkWidget, pour ne pas avoir de problèmes, il faut utiliser le système de macro offert par GTK+ pour transtyper notre GtkWidget en GtkContainer (eh oui en C, le polymorphisme a ses limites) :

main.c
gtk_container_add (GTK_CONTAINER (p_window), p_button);

Ce mécanisme de transtypage permet à GTK+ de vérifier que notre GtkWidget est bien compatible avec l'utilisation que l'on souhaite en faire. En cas d'échec, GTK+ nous prévient en affichant un message dans la console :

(gtk_bouton.exe:1044): Gtk-CRITICAL **: gtk_container_add: assertion `GTK_IS_CONTAINER (container)' failed

Pour finir, il faut connecter notre bouton pour appeler notre fonction cb_quit lorsque l'utilisateur clic dessus (ce qui correspond à l'événement "clicked") :

g_signal_connect (G_OBJECT (p_button), "clicked", G_CALLBACK (cb_quit), NULL);

Pour éviter d'appeler la fonction gtk_widget_show pour tous les widgets de notre application, on peut se contenter d'un seul appel à la fonction gtk_widget_show_all qui permet d'afficher tous les widgets contenus dans celui passé à la fonction.

main.c
#include <stdlib.h>
#include <gtk/gtk.h>
#include "callback.h"
int main (int argc, char **argv)
{
  GtkWidget *p_window = NULL;
  /* Initialisation de GTK+ */
  gtk_init (&argc, &argv);
  /* Creation de la fenetre principale de notre application */
  p_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  g_signal_connect (G_OBJECT (p_window), "destroy", G_CALLBACK (cb_quit), NULL);
  {
    GtkWidget *p_button = NULL;
    p_button = gtk_button_new_from_stock (GTK_STOCK_QUIT);
    gtk_container_add (GTK_CONTAINER (p_window), p_button);
    g_signal_connect (G_OBJECT (p_button), "clicked", G_CALLBACK (cb_quit), NULL);
  }
  /* Affichage de la fenetre principale */
  gtk_widget_show_all (p_window);
  /* Lancement de la boucle principale */
  gtk_main ();
  return EXIT_SUCCESS;
}


GTK+ par l'exemple

Table des matières

  • IV. Comment afficher plusieurs widgets
    • IV-A. Aperçu
    • IV-B. Le problème
    • IV-C. La solutions

IV. Comment afficher plusieurs widgets

IV-A. Aperçu

IV-B. Le problème

Dans la partie précédente, nous avons réussi à afficher un bouton dans la fenêtre de notre application. Si vous avez essayé d'ajouter un second widget, GTK+ à dû vous répondre poliment :

(gtk_box.exe:492) : Gtk-WARNING **: Attempting to add a widget with type
GtkButton to a GtkWindow, but as a GtkBin subclass a GtkWindow can only contain
one widget at a time; it already contains a widget of type GtkButton

Les plus anglophiles d'entre vous auront compris que GTK+ n'accepte pas l'ajout un second widget dans notre fenêtre tout simplement parce qu'il y en a déjà un et que la sous classe GtkBin (classe mère de notre GtkWindow) ne peut contenir qu'un seul widget.

IV-C. La solutions

Pour contourner ce problème, il faut commencer par créer un widget qui accepte d'en contenir plusieurs autres, pour ensuite y ajouter tout ce dont nous avons besoin. Pour l'instant, nous utiliserons qu'un seul widget (il existe trois classes qui permettent ceci), il s'agit des GtkBox. Il s'agit de la méthode que j'utilise le plus souvent. Ce n'est peut-être pas la plus simple à maîtriser mais elle extrêmement souple et puissante. Elle consiste à créer une boîte puis à y ajouter les widgets les uns après les autres (soit au début soit à la fin). Il existe deux classes héritant de GtkBox : les GtkVBox qui empilent les widgets dans le sens vertical et les GtkHBox qui font de même mais dans le sens horizontal. Les fonctions pour manipuler ces deux classes sont les mêmes, seule la fonction pour les créer portent un nom différent :

GtkWidget *gtk_vbox_new (gboolean homogeneous, gint spacing);
GtkWidget *gtk_hbox_new (gboolean homogeneous, gint spacing);

Le paramètre homogeneous permet de réserver pour chaque widget une zone de taille identique (zone que le widget n'est pas obligé de remplir) et spacing permet d'ajouter une bordure en pixels (espacement autour de la GtkBox). Ensuite, il suffit d'ajouter les différents widgets grâce aux fonctions :

void gtk_box_pack_start (GtkBox *box, GtkWidget *child, gboolean expand, gboolean fill, guint padding);
void gtk_box_pack_end (GtkBox *box, GtkWidget *child, gboolean expand, gboolean fill, guint padding);

Qui ajoutent réciproquement le widget child au début et à la fin de box. Le paramètre expand permet au widget d'avoir le plus de place possible (si le paramètre homogeneous vaut TRUE, cette option a aucun effet). Si plusieurs widget ont ce paramètre à TRUE, ils se partagent de façon égale l'espace. L'option fill permet au widget de remplir tout l'espace qui lui ait réservé.
Pour réellement comprendre l'influence de ces paramètres, il faut faire des tests en modifiant un à un chaque paramètre et observer les effets d'un redimentionnement de la fenêtre principale.
Le plus gênant avec cette méthode c'est qu'il faut jongler entre les GtkHBox et les GtkVBox en les imbriquant pour obtenir le résultat souhaiter : n'hésitez pas à utiliser une feuille et un crayon ;)

Pour en revenir à notre éditeur de texte, nous allons préparer notre application à contenir les futurs widgets. Nous utilisons un GtkVBox pour l'ensemble des widgets (il s'agit de la boîte principale qui pourra contenir d'autres GtkContainer selon nos besoins) :

main.c
#include <stdlib.h>
#include <gtk/gtk.h>
#include "callback.h"
int main (int argc, char **argv)
{
  GtkWidget *p_window = NULL;
  GtkWidget *p_main_box = NULL;
  /* Initialisation de GTK+ */
  gtk_init (&argc, &argv);
  /* Creation de la fenetre principale de notre application */
  p_window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  g_signal_connect (G_OBJECT (p_window), "destroy", G_CALLBACK (cb_quit), NULL);
  /* Creation du conteneur principal */
  p_main_box = gtk_vbox_new (FALSE, 0);
  gtk_container_add (GTK_CONTAINER (p_window), p_main_box);
  /* Creation du bouton "Quitter" */
  {
    GtkWidget *p_button = NULL;
    p_button = gtk_button_new_from_stock (GTK_STOCK_QUIT);
    g_signal_connect (G_OBJECT (p_button), "clicked", G_CALLBACK (cb_quit), NULL);
    gtk_box_pack_start (GTK_BOX (p_main_box), p_button, FALSE, FALSE, 0);
  }
  /* Affichage de la fenetre principale */
  gtk_widget_show_all (p_window);
  /* Lancement de la boucle principale */
  gtk_main ();
  return EXIT_SUCCESS;
}


GTK+ par l'exemple

Table des matières

  • V. Afficher le contenue d'un fichier
    • V-A. Aperçu
    • V-B. Saisir du texte
    • V-C. Ouvrir un fichier
    • V-D. La petite touche finale
    • V-E. Code source

V. Afficher le contenue d'un fichier

V-A. Aperçu

V-B. Saisir du texte

Les choses sérieuses vont commencer, il s'agit de mettre en place le widget qui va nous permettre d'afficher et d'éditer du texte. Il s'agit de la classe GtkTextView. Grâce à GTK+ la mise en place est toujours aussi simple :

main.c
  GtkWidget *p_text_view = NULL;
  /* ... */
  /* Creation de la zone de texte */
  p_text_view = gtk_text_view_new ();
  gtk_box_pack_start (GTK_BOX (p_main_box), p_text_view, TRUE, TRUE, 0);

Comme il s'agit de la partie centrale de notre application, nous demandons à ce qu'elle prenne le plus de place possible (grâce à la fonction gtk_box_pack_start).
Puisque nous voulons afficher les boutons en dessous de la zone de texte, il faut ajouter ce morceau de code avant celui créant le bouton.

V-C. Ouvrir un fichier

Maintenant nous avons une belle zone de texte, il faut la remplir avec le contenu d'un fichier. Pour ce faire, nous allons commencer par ajouter un bouton ouvrir :

main.c
  /* Creation du bouton "Ouvrir" */
  {
    GtkWidget *p_button = NULL;
    p_button = gtk_button_new_from_stock (GTK_STOCK_OPEN);
    g_signal_connect (G_OBJECT (p_button), "clicked", G_CALLBACK (cb_open), p_text_view);
    gtk_box_pack_start (GTK_BOX (p_main_box), p_button, FALSE, FALSE, 0);
  }

Si vous exécutez le programme maintenant (3), vous remarquerez que les boutons sont ajoutés les uns en dessous des autres, ce qui diminue la zone de saisie (avec deux boutons ce n'est pas gênant mais au bout d'une dizaine ça risque d'être problématique). Pour pallier ce problème, nous allons utiliser un nouveau style de GtkBox : les GtkButtonBox qui sont prévus pour contenir des boutons (ça tombe plutôt bien :D). Donc pour éviter de diminuer la zone de saisie et comme nous avons de la place en largueur, nous allons utiliser un GtkHButtonBox qui va être inclus dans la p_main_box. Voici les modifications à effectuer :

main.c
  GtkWidget *p_button_box = NULL;
  /* ... */
  /* Creation du conteneur pour les boutons */
  p_button_box = gtk_hbutton_box_new ();
  gtk_box_pack_start (GTK_BOX (p_main_box), p_button_box, FALSE, FALSE, 0);
  /* ... */
  gtk_box_pack_start (GTK_BOX (p_button_box), p_button, FALSE, FALSE, 0);
  /* ... */
  gtk_box_pack_start (GTK_BOX (p_button_box), p_button, FALSE, FALSE, 0);

Maintenant il nous reste plus qu'à créer la fonction cb_open dans le fichier callback.c, pour l'instant nous nous contenterons d'ouvrir toujours le même fichier, nous verrons plus tard comment laisser le choix à l'utilisateur :

callback.c
#define DEFAULT_FILE "main.c"
void cb_open (GtkWidget *p_widget, gpointer user_data)
{
  open_file (DEFAULT_FILE, GTK_TEXT_VIEW (user_data));
  /* Parametre inutilise */
  (void)p_widget;
}

Vous aurez compris que l'on va déléguer tout le travail à la fonction open_file, qui devra ouvrir le fichier dont le nom est passé en premier paramètre pour l'afficher dans le GktTextView.
J'ai fait ce choix pour deux raisons : le code d'une fonction callback peut être très conséquent (surtout si l'on souhaite qu'elle affiche une boîte de dialogue), et aussi parce que cb_open n'est peut être pas la seule fonction à copier un fichier dans un GtkTextView (par exemple lors de la création d'un nouveau fichier, l'on peut vouloir copier un squelette de fichier).
Pour copier notre fichier, plutôt que d'utiliser la fonction fgets, nous allons nous servir de la glib (4) qui propose la fonction :

gboolean g_file_get_contents (const gchar *filename,  gchar **contents, gsize *length, GError **error);

Qui nous renvoit le contenu du fichier nommé filename dans le tampon contents. Il est possible de récupérer le nombre de caractères copiés et retourne TRUE en cas de succès, sinon FALSE et dans ce cas une structure de type GError est crée pour en savoir plus sur le type d'erreur rencontré :

callback.c
static void open_file (const gchar *file_name, GtkTextView *p_text_view)
{
  g_return_if_fail (file_name && p_text_view);
  {
    gchar *contents = NULL;
    if (g_file_get_contents (file_name, &contents, NULL, NULL))
    {
      /* Copie de contents dans le GtkTextView */
    }
    else
    {
        print_warning ("Impossible d'ouvrir le fichier %s\n", file_name);
    }
  }
}

Je pense qu'un certain nombre d'explications s'impose (surtout si je vais trop vite, n'hésitez pas à m'arrêter !) :

  • g_return_if_fail est une macro qui peut être comparée à assert sauf qu'elle se contente de sortir de la fonction si l'expression passée en paramètre est fausse. C'est une méthode pratique pour vérifier la validité des paramètres (si la fonction doit retourne une valeur, utilisé plutôt g_return_val_if_fail, qui prend un second paramètre la valeur à retourner)
  • Ensuite on essaie de récupérer le contenu de notre fichier
  • Si cela échoue, nous le signalons à l'utilisateur à l'aide de la fonction print_warning.
  • Le type gchar est une redéfinition du type char par la glib (il en va de même pour tous les autres types du C), privilégiez ces types lorsque vous utilisez des fonctions de cette bibliothèques.

Mais qu'est-ce donc cette fonction print_warning ? C'est une fonction que nous allons créer, semblable à printf qui signalera à l'utilisateur qu'une erreur non critique est survenue. Comme nous n'avons pas encore abordé les boîtes de dialogue pour afficher un message, il s'agira pour l'instant d'une simple redéfinition de printf. Pendant que nous sommes dans l'affichage des messages, nous allons aussi créer deux fonctions print_info et print_error qui vont respectivement afficher un message d'information et un message d'erreur critique (ce qui entraîne la terminaison du programme), tout ceci dans un nouveau fichier error.c :

error.h
#ifndef H_ERROR
#define H_ERROR
void print_info (char *, ...);
void print_warning (char *, ...);
void print_error (char *, ...);
#endif /* not H_ERROR */
error.c
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include "error.h"
void print_info (char *format, ...)
{
  va_list va;
  va_start (va, format);
  printf ("Information : ");
  vprintf (format, va);
  printf ("\n");
}
void print_warning (char *format, ...)
{
  va_list va;
  va_start (va, format);
  fprintf (stderr, "Erreur : ");
  vfprintf (stderr, format, va);
  fprintf (stderr, "\n");
}
void print_error (char *format, ...)
{
  va_list va;
  va_start (va, format);
  fprintf (stderr, "Erreur fatale : ");
  vfprintf (stderr, format, va);
  fprintf (stderr, "\n");
  exit (EXIT_FAILURE);
}

Pour en finir avec l'ouverture d'un fichier, il nous reste à copier contents dans le GtkTextView. En fait le GtkTextView ne gère que la forme, pour modifier le contenu, il faut passer par les GtkTextBuffer. Commençons donc par récupérer ce fameux GtkTextBuffer :

callback.c
      GtkTextBuffer *p_text_buffer = NULL;
      p_text_buffer = gtk_text_view_get_buffer (p_text_view);

Et pour finir on utilise la fonction :

void gtk_text_buffer_insert (GtkTextBuffer *buffer, GtkTextIter *iter, const gchar *text, gint len);

Récapitulons : buffer est le GtkTextBuffer que l'on vient de récupérer, text c'est le texte à afficher et len la taille de ce dernier (ou -1 s'il s'agit d'une chaîne de caractères au sens du langage C).
Mais qu'est-ce donc ce GtkTextIter ? On peut voir cela comme un curseur virtuel qui indique la position à laquelle effectuer l'insertion de texte dans le GtkTextBuffer (5). Allons voir ce que la documentation peut nous apprendre à leur sujet (6) :

typedef struct {
  /* GtkTextIter is an opaque datatype; ignore all these fields.
   * Initialize the iter with gtk_text_buffer_get_iter_*
   * functions
   */
} GtkTextIter;

Voilà, on a notre solution : suffit de rechercher les fonctions commençant par gtk_text_buffer_get_iter_, voici la liste :

void gtk_text_buffer_get_iter_at_line_offset (GtkTextBuffer *buffer, GtkTextIter *iter, gint line_number, gint char_offset);
void gtk_text_buffer_get_iter_at_offset (GtkTextBuffer *buffer, GtkTextIter *iter, gint char_offset);
void gtk_text_buffer_get_iter_at_line (GtkTextBuffer *buffer, GtkTextIter *iter, gint line_number);
void gtk_text_buffer_get_iter_at_line_index (GtkTextBuffer *buffer, GtkTextIter *iter, gint line_number, gint byte_index);
void gtk_text_buffer_get_iter_at_mark (GtkTextBuffer *buffer, GtkTextIter *iter, GtkTextMark *mark);
void gtk_text_buffer_get_iter_at_child_anchor (GtkTextBuffer *buffer, GtkTextIter *iter, GtkTextChildAnchor *anchor);

La fonction gtk_text_buffer_get_iter_at_line fait parfaitement l'affaire :

callback.c
      GtkTextIter iter;
      /* ... */
      gtk_text_buffer_get_iter_at_line (p_text_buffer, &iter, 0);
      gtk_text_buffer_insert (p_text_buffer, &iter, contents, -1);

Cependant si vous ne voulez pas avoir de problèmes avec le codage des caractères, il vaut mieux convertir le codage local vers utf8. Voici le résultat final :

      gchar *utf8 = NULL;
      GtkTextIter iter;
      GtkTextBuffer *p_text_buffer = NULL;
      p_text_buffer = gtk_text_view_get_buffer (p_text_view);
      gtk_text_buffer_get_iter_at_line (p_text_buffer, &iter, 0);
      utf8 = g_locale_to_utf8 (contents, -1, NULL, NULL, NULL);
      g_free (contents), contents = NULL;
      gtk_text_buffer_insert (p_text_buffer, &iter, utf8, -1);
      g_free (utf8), utf8 = NULL;

V-D. La petite touche finale

Pour finir cette laborieuse partie sur une petite touche esthétique, nous allons demander à GTK+ de nous lancer l'application en plein écran. Pour se faire, il suffit d'ajouter, lors de la création de la fenêtre principale :

main.c
gtk_window_maximize (GTK_WINDOW (p_window));

On peut même se permettre une pointe d'excentricité en modifiant le titre de notre fenêtre :

main.c
gtk_window_set_title (GTK_WINDOW (p_window), "Editeur de texte en GTK+");


GTK+ par l'exemple

Table des matières

  • VI. Choisir un fichier
    • VI-A. Aperçu
    • VI-B. Utilisation d'un GtkFileChooserFile
    • VI-C. Code source

VI. Choisir un fichier

VI-A. Aperçu

VI-B. Utilisation d'un GtkFileChooserFile

Jusqu'à présent l'utilisateur n'avait pas le choix quant au fichier à ouvrir, heureusement GTK+ vient à notre aide grâce au widget GtkFileChooserFile qui est tout simplement une boîte de dialogue qui propose à l'utilisateur de sélectionner un fichier dans son arborescence disque.
Commençons par créer notre boîte de dialogue dans la fonction cb_open :

callback.c
Sélectionnez
void cb_open (GtkWidget *p_widget, gpointer user_data)
{
  GtkWidget *p_dialog = NULL;
  p_dialog = gtk_file_chooser_dialog_new ("Ouvrir un fichier", NULL,
                                          GTK_FILE_CHOOSER_ACTION_OPEN,
                                          GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                                          GTK_STOCK_OPEN, GTK_RESPONSE_ACCEPT,
                                          NULL);
  /* ... */
}

Cette fonction nécessite le titre de la boîte de dialogue, une fenêtre parente, le type d'action (ici, on souhaite ouvrir un fichier, GTK+ autorisera l'utilisateur à sélectionner uniquement les fichiers existants). Pour finir, il s'agit d'un couple de valeurs GTK_STOCK_ITEM/GTK_STOCK_RESPONSE qui permet de créer un bouton utilisant le stock ID spécifié et, lorsque celui-ci est cliqué, la fonction servant à lancer la boîte de dialogue retournera le réponse ID correspondant. Il existe une série de valeurs définies par GTK+, qu'il est conseillé d'utiliser :

Sélectionnez
typedef enum
{
  /* GTK returns this if a response widget has no response_id,
   * or if the dialog gets programmatically hidden or destroyed.
   */
  GTK_RESPONSE_NONE = -1,
  /* GTK won't return these unless you pass them in
   * as the response for an action widget. They are
   * for your convenience.
   */
  GTK_RESPONSE_REJECT = -2,
  GTK_RESPONSE_ACCEPT = -3,
  /* If the dialog is deleted. */
  GTK_RESPONSE_DELETE_EVENT = -4,
  /* These are returned from GTK dialogs, and you can also use them
   * yourself if you like.
   */
  GTK_RESPONSE_OK     = -5,
  GTK_RESPONSE_CANCEL = -6,
  GTK_RESPONSE_CLOSE  = -7,
  GTK_RESPONSE_YES    = -8,
  GTK_RESPONSE_NO     = -9,
  GTK_RESPONSE_APPLY  = -10,
  GTK_RESPONSE_HELP   = -11
} GtkResponseType;

Nous indiquons la fin des couples stock id/réponse id avec la valeur NULL.
Une fois la boîte de dialogue créee, on demande à GTK+ de l'afficher grâce à la fonction gtk_dialog_run, puis on récupère sa valeur de retour, qui correspond au bouton cliqué par l'utilisateur :

callback.c
Sélectionnez
  if (gtk_dialog_run (GTK_DIALOG (p_dialog)) == GTK_RESPONSE_ACCEPT)
  {
    /* ... */
  }

Ici ce qui nous intéresse, c'est lorsque que l'utilisateur clique sur le bouton ouvrir (donc gtk_dialog_run renvoie GTK_RESPONSE_ACCEPT), dans ce cas, il nous suffit de récupérer le nom du fichier sélectionné puis de l'ouvrir :

callback.c
Sélectionnez
    gchar *file_name = NULL;
    file_name = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (p_dialog));
    open_file (file_name, GTK_TEXT_VIEW (user_data));
    g_free (file_name), file_name = NULL;

Pour finir, dans tous les cas, on n'oublie pas de détruire la boîte de dialogue :

callback.c
Sélectionnez
  gtk_widget_destroy (p_dialog);

Et voilà le travail : l'utilisateur peut ouvrir le fichier qu'il souhaite.


GTK+ par l'exemple

Table des matières

  • VII. Interlude

VII. Interlude

Avant d'aller plus loin dans l'amélioration de l'interface de notre éditeur, il est nécessaire de modifier son fonctionnement interne (ne vous inquiétez je n'ai pas dit chambouler), en effet, souvenez-vous dans l'introduction j'ai évoqué la possibilités d'ouvrir plusieurs documents grâce à un système d'onglets. Pour cela, il faut sauvegarder les propriétés de chaque document ouvert. Pris à temps ce genre de modification n'est pas trop lourde, mais si nous attendons, cela risque de devenir un vrai casse tête !
Pour cela nous allons utiliser une structure de donnée pour chaque document ouvert qui devra garder en mémoire le chemin complet du fichier (ou NULL si le document n'est pas encore sauvegarder), s'il à était modifier depuis la dernière sauvegarde et le GtkTextView qui sert à son affichage. Voici donc la structure qui va nous servir :

document.h
typedef struct
{
  gchar *chemin;
  gboolean sauve;
  GtkTextView *p_text_view;
} document_t;

Sachant que nous serons amené à stocker plusieurs occurrences de cette structure, il serait bon de réfléchir dès maintenant à la structure de donnée à utiliser pour les stocker. La première idée qui vient à l'esprit est un tableau alloué dynamiquement, cependant l'utilisateur peut souhaiter fermer un document qui se trouve au milieu, on est alors obligé de décaler toutes les cellules en amont. Dans ce cas il parait naturel d'utiliser une liste chaînée. Par chance la glib propose une bibliothèque de gestion des listes chaînées, cela nous fera gagner du temps le moment venu. Pour l'instant, on se contente de créer une structure de données qui contiendra tous les documents ouverts (stockée dans une GList) et un pointeur sur le document actif (actuellement comme nous n'avons qu'un document ouvert en même temps, on manipulera uniquement ce pointeur) :

document.h
typedef struct
{
  GList *tous;
  document_t *actif;
} docs_t;

Maintenant se pose le problème de savoir comment transmettre cette structure. On pourrait se servir du paramètre user_data présent dans toutes les fonctions callback mais certaines fonctions utilise déjà ce paramètre (la fonction cb_open par exemple), il faudrait créer un champs supplémentaire dans notre structure documents_s. Une solution plus simple est de créer une variable globale à l'ensemble du programme. Ceux qui passe régulièrement sur le forum C savent que c'est fortement déconseillé, mais ici nous nous trouvons dans l'une des rares exceptions à la règle. De toute façon cela revient au même que de passer l'adresse de notre structure à toutes les fonctions callback. Nous définissons donc un nouveau fichier d'en-tête :

document.h
#ifndef H_DOCUMENT
#define H_DOCUMENT
#include <gtk/gtk.h>
typedef struct
{
  gchar *chemin;
  gboolean sauve;
  GtkTextView *p_text_view;
} document_t;
typedef struct
{
  GList *tous;
  document_t *actif;
} docs_t;
extern docs_t docs;
#endif /* not H_DOCUMENT */

Et dans le fichier main.c :

main.c
#include "document.h"
docs_t docs = {NULL, NULL};

Pour l'instant la seule fonction qui modifie l'état d'un document est la fonction open_file, il faut donc inclure document.h dans callback.c et modifier légèrement la fonction pour enregistrer le document ouvert :

callback.c
    if (g_file_get_contents (file_name, &contents, NULL, NULL))
    {
      /* Pour l'instant il faut allouer la memoire, par la suite on modifira
         simplement le pointeur */
      docs.actif = g_malloc (sizeof (*docs.actif));
      docs.actif->chemin = g_strdup (file_name);
      /* Pour l'instant, on se contente de stocker le seul GtkTextView qui existe,
        par la suite, il faudra en creer un nouveau ! */
      docs.actif->p_text_view = p_text_view;
      /* Le document vient d'etre ouvert, il n'est donc pas modifie */
      docs.actif->sauve = TRUE;
      /* ... */
    }

Ne soyez pas choqué si je ne teste pas le retour de la fonction g_malloc pour vérifier qu'il n'est pas NULL. En effet, cette dernière met fin à l'application si l'allocation échoue.


GTK+ par l'exemple

Table des matières

  • VIII. Sauvegarder les modification
    • VIII-A. Aperçu
    • VIII-B. Enregistrer
    • VIII-C. Enregistrer sous

VIII. Sauvegarder les modification

VIII-A. Aperçu

VIII-B. Enregistrer

Avant de pouvoir sauvegarder un document, il faut qu'il soit modifié. Comment savoir que le contenu du fichier a été modifier ? Grâce au signal changed du GtkTextBuffer que nous interceptons grâce à la fonction cb_modifie :

main.c
  {
    GtkTextBuffer *p_text_buffer = NULL;
    p_text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (p_text_view));
    g_signal_connect (G_OBJECT (p_text_buffer), "changed", G_CALLBACK (cb_modifie), NULL);
  }

Cette dernière modifie simplement le champ sauve du document :

callback.c
void cb_modifie (GtkWidget *p_widget, gpointer user_data)
{
  if (docs.actif)
  {
    docs.actif->sauve = FALSE;
  }
}

Pour la sauvegarde, il faut distinguer deux cas : soit il s'agit de la première sauvegarde du fichier, il faut alors demander le nom du fichier à l'utilisateur (cela ressemble fortement à l'ouverture d'un fichier), soit le fichier est déjà présent sur le disque, il suffit de remplacer son contenu par celui du GtkTextViewer. Nous avons convenu qu'un fichier qui n'était pas encore enregistré avait sa propriété chemin à NULL.
Bien sûr cela n'a lieu que si un fichier est ouvert et qu'il a été modifié depuis sa dernière sauvegarde :

callback.c
void cb_save (GtkWidget *p_widget, gpointer user_data)
{
  if (docs.actif)
  {
    if (!docs.actif->sauve)
    {
      /* Le fichier n'a pas encore ete enregistre */
      if (!docs.actif->chemin)
      {
        GtkWidget *p_dialog = NULL;
        p_dialog = gtk_file_chooser_dialog_new ("Sauvegarder le fichier", NULL,
                                                GTK_FILE_CHOOSER_ACTION_SAVE,
                                                GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
                                                GTK_STOCK_SAVE, GTK_RESPONSE_ACCEPT,
                                                NULL);
        if (gtk_dialog_run (GTK_DIALOG (p_dialog)) == GTK_RESPONSE_ACCEPT)
        {
          docs.actif->chemin = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (p_dialog));
        }
        gtk_widget_destroy (p_dialog);
      }
      /* Soit le fichier a deja ete enregistre, soit l'utilisateur vient de nous
         fournir son nouvel enplacement, on peut donc l'enregistrer */
      if (docs.actif->chemin)
      {
        /* ... */
      }
    }
  }
  else
  {
    print_warning ("Aucun document ouvert");
  }
}

Il nous reste plus qu'à récupérer le contenu du GtkTextViewer et le copier dans le fichier chemin sans oublier de reconvertir le texte dans le codage local :

callback.c
        FILE *fichier = NULL;
        fichier = fopen (docs.actif->chemin, "w");
        if (fichier)
        {
          gchar *contents = NULL;
          gchar *locale = NULL;
          GtkTextIter start;
          GtkTextIter end;
          GtkTextBuffer *p_text_buffer = NULL;
          p_text_buffer = gtk_text_view_get_buffer (docs.actif->p_text_view);
          gtk_text_buffer_get_bounds (p_text_buffer, &start, &end);
          contents = gtk_text_buffer_get_text (p_text_buffer, &start, &end, FALSE);
          locale = g_locale_from_utf8 (contents, -1, NULL, NULL, NULL);
          g_free (contents), contents = NULL;
          fprintf (fichier, "%s", locale);
          g_free (locale), locale = NULL;
          fclose (fichier), fichier = NULL;
          docs.actif->sauve = TRUE;
        }
        else
        {
          print_warning ("Impossible de sauvegarder le fichier %s", docs.actif->chemin);
        }

Le dernier paramètre de la fonction gtk_text_buffer_get_text est mis à FALSE car nous ne voulons pas enregistrer les caractères invisibles présent dans le GtkTextBuffer. Ces caractères servent à mettre en forme le texte (taille, couleur...), nous pourrions créer un nouveau format de fichier pour enregistrer la mise en forme.

VIII-C. Enregistrer sous

Pour enregistrer notre document sous un autre nom, il suffit de faire croire à cb_save que le document n'a pas encore était enregistré, tout simplement en changeant chemin à NULL et sauve à FALSE :

callback.c
void cb_saveas (GtkWidget *p_widget, gpointer user_data)
{
  if (docs.actif)
  {
    document_t tmp = *docs.actif;
    docs.actif->chemin = NULL;
    docs.actif->sauve = FALSE;
    cb_save (p_widget, user_data);
    if (!docs.actif->sauve)
    {
      (*docs.actif) = tmp;
    }
  }
  else
  {
    print_warning ("Aucun document ouvert");
  }
}

Il ne faut pas oublier que l'utilisateur peut annuler l'enregistrement, de ce fait, nous sauvegardons l'état du document et si la sauvegarde n'a pas eu lieu, on le restaure.


GTK+ par l'exemple

Table des matières

  • IX. Créer un nouveau document
    • IX-A. Aperçu
    • IX-B. Nouveau fichier

IX. Créer un nouveau document

IX-A. Aperçu

IX-B. Nouveau fichier

Maintenant que l'utilisateur peut ouvrir et fermer un fichier, il est intéressant de lui donner la possibilité d'en créer un nouveau. Je ne reviens pas sur la création du bouton (ça devrait déjà être fait :D), passons directement à la fonction cb_new :

callback.c
void cb_new (GtkWidget *p_widget, gpointer user_data)
{
  /* Pour l'instant il faut allouer la memoire, par la suite on modifiera
     simplement le pointeur */
  docs.actif = g_malloc (sizeof (*docs.actif));
  docs.actif->chemin = NULL;
  /* Pour l'instant, on se contente de stocker le seul GtkTextView qui existe,
    par la suite, il faudra en creer un nouveau ! */
  docs.actif->p_text_view = GTK_TEXT_VIEW (user_data);
  /* Le document vient d'etre creer, il n'est donc pas modifie */
  docs.actif->sauve = TRUE;
  gtk_widget_set_sensitive (GTK_WIDGET (docs.actif->p_text_view), TRUE);
}

Je ne sais pas vous mais pour ma part j'ai fait un copier/coller de la fonction open_file ! Et oui, ouvrir un document peut se résumer à créer un document vide puis remplir le GtkTextView avec le fichier souhaité ! On peut donc simplifier notre fonction open_file ainsi :

callback.c
static void open_file (const gchar *file_name, GtkTextView *p_text_view)
{
  g_return_if_fail (file_name && p_text_view);
  {
    gchar *contents = NULL;

    if (g_file_get_contents (file_name, &contents, NULL, NULL))
    {
      /* Copie de contents dans le GtkTextView */
      GtkTextIter iter;
      GtkTextBuffer *p_text_buffer = NULL;

      cb_new (NULL, p_text_view);
      gtk_widget_set_sensitive (GTK_WIDGET (docs.actif->p_text_view), TRUE);
      p_text_buffer = gtk_text_view_get_buffer (p_text_view);
      gtk_text_buffer_get_iter_at_line (p_text_buffer, &iter, 0);
      gtk_text_buffer_insert (p_text_buffer, &iter, contents, -1);
      /* Nous sommes obliges de remetre sauve a TRUE car l'insertion du contenu
         du fichier dans le GtkTextView a appele cb_modfie */
      docs.actif->sauve = TRUE;
    }
    else
    {
        print_warning ("Impossible d'ouvrir le fichier %s\n", file_name);
    }
  }
}

C'est beau la factorisation du code ! Par contre nous sommes obligés de remettre sauve à TRUE car nous avons modifié le GtkTextBuffer.


GTK+ par l'exemple

Table des matières

  • X. Fermer
    • X-A. Aperçu
    • X-B. Fermer un fichier
    • X-C. Enregistrer avant de fermer

X. Fermer

X-A. Aperçu

X-B. Fermer un fichier

Maintenant que nous allouons de la mémoire, il faut la libérer à un endroit. Logiquement cela va être fait à la fermeture du document, en guise d'exercice, je vous laisse créer le bouton dans notre boîte à bouton...
...
Allez je vous aide, le stock id correspondant est GTK_STOCK_CLOSE.
Voilà maintenant, si tout c'est bien passé, vous devriez être arrivé dans le fichier callback.c avec quelque chose ressemblant à :

callback.c
void cb_close (GtkWidget *p_widget, gpointer user_data)
{
  /* ... */
}

Lorsque l'on ferme un document, il faut vider le GtkTextView (avec les onglets, on se contentera de le supprimer), pour cela, il faut récupérer le début et la fin du GtkTextBuffer et demander à GTK+ de supprimer tout ce qui ce trouve entre les deux itérateurs :

callback.c
  /* Avant de fermer, il faut verifier qu'un document a bien ete ouvert */
  if (docs.actif)
  {
    GtkTextIter start;
    GtkTextIter end;
    GtkTextBuffer *p_text_buffer = NULL;
    p_text_buffer = gtk_text_view_get_buffer (docs.actif->p_text_view);
    gtk_text_buffer_get_bounds (p_text_buffer, &start, &end);
    gtk_text_buffer_delete (p_text_buffer, &start, &end);
    /* ... */
  }
  else
  {
    print_warning ("Aucun document ouvert");
  }

Bien sûr il ne faut pas oublier de libérer la mémoire

callback.c
    g_free (docs.actif->chemin), docs.actif->chemin = NULL;
    docs.actif->p_text_view = NULL;
    g_free (docs.actif), docs.actif = NULL;

Pour symboliser la fermeture d'un document, nous désactivons le GtkTextView lors de la fermeture (ne pas oublier de le faire aussi dans main.c) :

callback.c
    gtk_widget_set_sensitive (GTK_WIDGET (docs.actif->p_text_view), FALSE);

Et nous le réactivons lors de l'ouverture si celle si réussie :

callback.c
    gtk_widget_set_sensitive (GTK_WIDGET (docs.actif->p_text_view), TRUE);

Lorsque l'utilisateur quitte l'application, il faut bien sûr fermer le document, s'il y en a un ouvert :

void cb_quit (GtkWidget *p_widget, gpointer user_data)
{
  if (docs.actif)
  {
    cb_close (p_widget, user_data);
  }
  gtk_main_quit();
}

Et aussi lorsqu'il créé un nouveau document (et par conséquent lorsqu'il ouvre un fichier puisque l'on fait appel à cb_new) :

void cb_new (GtkWidget *p_widget, gpointer user_data)
{
  if (docs.actif)
  {
    cb_close (p_widget, user_data);
  }
  /* ... */
}

X-C. Enregistrer avant de fermer

Si le document a été modifié depuis la dernière sauvegarde, généralement le programme le signale à l'utilisateur et lui propose de sauvegarder ou d'annuler la fermeture. Pour se faire, nous devons modifier la fonction cb_close, avant de fermer le document, nous allons afficher une boîte de dialogue.
Pour créer une boîte de dialogue simplement, il existe la classe GtkDialog et comme nous avons besoin de boutons (pour que l'utilisateur fasse son choix), nous allons créer notre boîte avec la fonction :

GtkWidget *gtk_dialog_new_with_buttons (const gchar *title, GtkWindow *parent, GtkDialogFlags flags, const gchar *first_button_text, ...);

Nous retrouvons les mêmes options que pour le GtkFileChooserDialog, mis à part option du type GtkDialogFlags :

typedef enum
{
  GTK_DIALOG_MODAL               = 1 << 0, /* appel gtk_window_set_modal (win, TRUE) */
  GTK_DIALOG_DESTROY_WITH_PARENT = 1 << 1, /* appel gtk_window_set_destroy_with_parent () */
  GTK_DIALOG_NO_SEPARATOR        = 1 << 2  /* Pas de barre de separation au dessus des boutons */
} GtkDialogFlags;

Nous nous contenterons de rendre notre fenêtre modale (7).
Pour avoir accès à notre fenêtre principale, nous ajoutons un champs p_main_window à notre structure globale docs_t que nous initialisons dans la fonction main :

main.c
  docs.p_main_window = GTK_WINDOW (p_window);

Il nous reste plus qu'a créer notre boîte de dialogue avec trois boutons : Oui, Non, Annuler :

callback.c
    if (!docs.actif->sauve)
    {
      GtkWidget *p_dialog = NULL;
      p_dialog = gtk_dialog_new_with_buttons ("Sauvegarder",
                                              docs.p_main_window,
                                              GTK_DIALOG_MODAL,
                                              GTK_STOCK_YES, GTK_RESPONSE_YES,
                                              GTK_STOCK_NO, GTK_RESPONSE_NO,
                                              GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL, NULL);
      /* ... */

Nous obtenons donc une structure de type GtkDialog :

typedef struct {
  GtkWidget *vbox;
  GtkWidget *action_area;
} GtkDialog;

Nous remarquons que cette structure contient deux membres qui permettent d'accéder à une GtkBox, où nous allons ajouter le contenu de la fenêtre, et à la partie contenant les boutons.
Ensuite la construction de la fenêtre est identique à celle de la fenêtre principale, ici nous nous contenterons d'ajouter un GtkLabel :

callback.c
      GtkWidget *p_label = NULL;
      /* ... */
      p_label = gtk_label_new ("Voulez-vous sauvegarder les modifications ?");
      gtk_box_pack_start (GTK_BOX (GTK_DIALOG (p_dialog)->vbox), p_label, TRUE, TRUE, 0);

Une fois notre fenêtre prête, il suffit de faire appel à la fonction gtk_dialog_run qui va afficher notre GtkDialog et nous retourner le réponse id correspondant au bouton cliqué par l'utilisateur :

callback.c
      switch (gtk_dialog_run (GTK_DIALOG (p_dialog)))
      {
        case GTK_RESPONSE_YES:
          cb_save (p_widget, user_data);
        break;
        case GTK_RESPONSE_NO:
        break;
        case GTK_RESPONSE_CANCEL:
          gtk_widget_destroy (p_dialog);
          return;
        break;
      }
      gtk_widget_destroy (p_dialog);
    }
    /* ... */

Si l'utilisateur clique sur non, on ne fait rien (le document sera simplement fermé), sur oui, on enregistre avant de fermer et pour finir, s'il annule on quitte la fonction.


GTK+ par l'exemple

Table des matières

  • XI. Les barres de défilement
    • XI-A. Aperçu
    • XI-B. Ajouter des barres de défilement

XI. Les barres de défilement

XI-A. Aperçu

Image non disponible

XI-B. Ajouter des barres de défilement

Après le dernier chapitre, quelque peu laborieux, voici une partie plus simple mais qui va rendre notre application plus pratique. En effet, si vous avez essayé d'ouvrir un fichier de grande taille, vous avez pu remarquer que pour pouvoir lire la fin du fichier, il fallait utiliser les touches du clavier : pas très convivial.
Pour rendre la navigation plus aisée, on utilise des barres de défilement :

main.c
    GtkWidget *p_scrolled_window = NULL;
    p_scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    gtk_box_pack_start (GTK_BOX (p_main_box), p_scrolled_window, TRUE, TRUE, 0);
    /* Creation de la zone de texte */
    /* ... */
    gtk_container_add (GTK_CONTAINER (p_scrolled_window), p_text_view);

Le constructeur de notre GtkScrolledWindow prend en argument deux GtkAdjustment qui permettent de définir différentes propriétés de la barre de défilement (taille d'une page, la position de départ...), nous laissons GTK+ faire en passant NULL.
C'est un bon début mais esthétiquement on peut faire mieux : même s'il n'est pas utile d'avoir une barre de défilement, elle est quand même affichée. On peut demander à GTK+ de les afficher que si cela est nécessaire :

main.c
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (p_scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);

En fait nous avons de la chance car la classe GtkTextView fait partie des widget qui supporte de façon native les barres de défilement. Comment le savoir ? Ce genre de widget possède une ou deux propriétés de type GtkAdjustment (une pour la barre verticale, l'autre pour la barre horizontale). Actuellement seulement trois classes en sont capables : les GtkTextView, les GtkTreeView et les GtkLayout.
Comment faire pour les autres widgets ? Il faut passer par une classe adaptateur GtkViewport afin de créer les GtkAdjustment.


GTK+ par l'exemple

Table des matières

  • >XII. Les menus
    • XII-A. Aperçu
    • XII-B. Création du menu

XII. Les menus

XII-A. Aperçu

Image non disponible

XII-B. Création du menu

Pour simplifier nous avons utilisé des boutons pour permettre à l'utilisateur de créer un document, ouvrir un fichier... Mais il est plus courant d'utiliser un menu pour afficher ces options. GTK+ propose trois manières de créer un menu :

  • GtkItemFactory : cette méthode est obsolète depuis la version 2.4 de GTK+
  • GtkUIManager : c'est ce qu'on appel une usine à gaz, pour en savoir plus, vous pouvez vous reporter au tutoriel Utilisation de GtkUIManager
  • GtkMenu : c'est ce widget que nous allons étudier, il permet de créer un menu manuellement (à l'opposé de GtkUIManager).

Un menu, selon GTK+, est composé de plusieurs éléments :

  • GtkMenuBar : la barre de menu elle-même
  • GtkMenu : la partie déroulante qui contient les différents éléments
  • GtkMenuItem : c'est sur ce widget que l'utilisateur clique pour lancer une action.

Pour commencer, faisons l'inventaire de ce que nous avons besoin :

  • Un GtkMenuBar qui sert de base au menu
  • Un GtkMenu, pour commencer nous nous aurons d'un menu Fichier
  • Six GtkMenuItem pour remplacer nos six boutons.

Commençons par la création du menu, nous mettons ce code dans un nouveau fichier dont seul la fonction menu_new sera accessible :

menu.c
GtkMenuBar *menu_new (gpointer user_data)
{
  GtkWidget *p_menu_bar = NULL;
  p_menu_bar = gtk_menu_bar_new ();
  /* ... */
  return GTK_MENU_BAR (p_menu_bar);
}

Le paramètre user_data nous servira lors de la connexion des callbacks (nous en avons besoin pour les fonctions cb_new et cb_open).
Ensuite, nous allons créer notre sous menu Fichier :

menu.c
  /* Menu "Fichier" */
  {
    GtkWidget *p_menu = NULL;
    GtkWidget *p_menu_item = NULL;
    p_menu = gtk_menu_new ();
    p_menu_item = gtk_menu_item_new_with_mnemonic ("_Fichier");
    /* ... */
    gtk_menu_item_set_submenu (GTK_MENU_ITEM (p_menu_item), p_menu);
    gtk_menu_shell_append (GTK_MENU_SHELL (p_menu_bar), p_menu_item);
  }
  return GTK_MENU_BAR (p_menu_bar);

Un sous menu est en fait un GtkMenuItem auquel on assigne un GtkMenu. Il faut bien sûr terminer la création du sous-menu en l'ajoutant à la barre de menu.
Pour finir, nous créons les éléments du menu, les GtkItemMenu. Il existe plusieurs types d'éléments (comparable aux différents types de bouton), pour commencer nous n'utiliserons que le type de base. Comme cette opération est répétitive, nous allons créer une fonction pour le faire :

menu.c
static void menu_item_new (GtkMenu *p_menu, const gchar *title, GCallback callback, gpointer user_data)
{
  GtkWidget *p_menu_item = NULL;
  p_menu_item = gtk_menu_item_new_with_mnemonic (title);
  gtk_menu_shell_append (GTK_MENU_SHELL (p_menu), p_menu_item);
  g_signal_connect (G_OBJECT (p_menu_item), "activate", callback, user_data);
}

Pour permettre à l'utilisateur d'accéder aux différents éléments du menu à l'aide de la touche <Alt>, nous utilisons des mnémoniques, par exemple pour quitter l'éditeur il suffit de faite Alt+F puis Q. Et voici le code pour créer nos six éléments du menu :

menu.c
  menu_item_new (GTK_MENU (p_menu), "_Nouveau", G_CALLBACK (cb_new), user_data);
  menu_item_new (GTK_MENU (p_menu), "_Ouvrir", G_CALLBACK (cb_open), user_data);
  menu_item_new (GTK_MENU (p_menu), "_Enregistrer", G_CALLBACK (cb_save), user_data);
  menu_item_new (GTK_MENU (p_menu), "Enregistrer _sous", G_CALLBACK (cb_saveas), user_data);
  menu_item_new (GTK_MENU (p_menu), "_Fermer", G_CALLBACK (cb_close), user_data);
  menu_item_new (GTK_MENU (p_menu), "_Quitter", G_CALLBACK (cb_quit), user_data);

Voilà, notre menu est créé ! il nous reste plus qu'à l'intégrer à notre fenêtre :

main.c

Le problème c'est que nous avons besoin de passer le GtkTextView lors de la création du menu (qui est utilisé par les fonctions cb_new et cb_open) or celui-ci est créé après le menu (puisque nous créons les widgets dans l'ordre où il faut les intégrer à l'interface (8), nous devons créer le GtkTextView (seul l'appel à gtk_text_view_new est déplacé).


GTK+ par l'exemple

Table des matières

  • XIII. Les barres d'outils
    • XIII-A. Aperçu
    • XIII-B. Création d'une barre d'outils

XIII. Les barres d'outils

XIII-A. Aperçu

Image non disponible

XIII-B. Création d'une barre d'outils

La création d'une barre d'outils ressemble fort à celle d'un menu, en plus simple puisqu'il n'y a pas de sous menu : on crée notre barre d'outils (gtk_toolbar_new) puis les éléments de la barre, pour cela on utilise des boutons (gtk_tool_button_new_from_stock), que l'on insére dans la barre (gtk_toolbar_insert). Pas conséquent notre fichier barreoutils.c ressemble à menu.c :

barreoutils.c
#include <gtk/gtk.h>
#include "callback.h"
#include "barreoutils.h"

static void toolbar_item_new (GtkToolbar *, const gchar *, GCallback, gpointer);

GtkToolbar *toolbar_new (gpointer user_data)
{
  GtkWidget *p_toolbar = NULL;

  p_toolbar = gtk_toolbar_new ();
  /* ... */
  toolbar_item_new (GTK_TOOLBAR (p_toolbar), GTK_STOCK_NEW, G_CALLBACK (cb_new), user_data);
  toolbar_item_new (GTK_TOOLBAR (p_toolbar), GTK_STOCK_OPEN, G_CALLBACK (cb_open), user_data);
  toolbar_item_new (GTK_TOOLBAR (p_toolbar), GTK_STOCK_SAVE, G_CALLBACK (cb_save), user_data);
  toolbar_item_new (GTK_TOOLBAR (p_toolbar), GTK_STOCK_SAVE_AS, G_CALLBACK (cb_saveas), user_data);
  toolbar_item_new (GTK_TOOLBAR (p_toolbar), GTK_STOCK_CLOSE, G_CALLBACK (cb_close), user_data);
  toolbar_item_new (GTK_TOOLBAR (p_toolbar), GTK_STOCK_QUIT, G_CALLBACK (cb_quit), user_data);
  return GTK_TOOLBAR (p_toolbar);
}

static void toolbar_item_new (GtkToolbar *p_toolbar, const gchar *stock_id, GCallback callback, gpointer user_data)
{
  GtkToolItem *p_tool_item = NULL;

  p_tool_item = gtk_tool_button_new_from_stock (stock_id);
  g_signal_connect (G_OBJECT (p_tool_item), "clicked", callback, user_data);
  gtk_toolbar_insert (p_toolbar, p_tool_item, -1);
}

Le code n'est pas complet car lorsque j'exécute le programme, GTK+ affiche, en plus des icônes, le texte sous les boutons. Pour modifier cela, il suffit d'ajouter une ligne de code :

barreoutils.c
  gtk_toolbar_set_style (GTK_TOOLBAR (p_toolbar), GTK_TOOLBAR_ICONS);

Pourquoi gtk_tool_button_new_from_stock retourne un GtkToolItem et non un GtkWidget comme nous avons été habitué jusqu'à présent. Il s'agit d'une bizarrerie de GTK+ dont je ne connais pas la raison.
Pour anticiper un changement d'interface (dans GTK+ 3.0 peut être ?), nous aurions pu écrire :

Gtkwidget *p_tool_item = NULL;

p_tool_item = GTK_WIDGET (gtk_tool_button_new_from_stock (stock_id));
g_signal_connect (G_OBJECT (p_tool_item), "clicked", callback, user_data);
gtk_toolbar_insert (p_toolbar, GTK_TOOL_ITEM (p_tool_item), -1);


GTK+ par l'exemple

Table des matières

  • XIV. Les racourcis clavier
    • XIV-A. Aperçu
    • XIV-B. Mise en place des raccourcis clavier

    XIV. Les racourcis clavier

    XIV-A. Aperçu

    Image non disponible

    XIV-B. Mise en place des raccourcis clavier

    Il ne faut pas confondre les raccourcis clavier et les mnémoniques, ces derniers permettent d'accéder à un élément séquentiellement alors que les raccourcis appellent une fonction si l'utilisateur appuie simultanément sur une ou plusieurs touches (généralement de la forme <Modificateur>Touche, où Modificateur représente les touches Shift, Alt et Control). Ce raccourci peut être attaché à un élément du menu mais ceci n'est pas obligatoire.
    Les raccourcis sont regroupés en groupe dans un GtkAccelGroup qu'il faut commencer par créer :

    raccourcis.c
    #include <gtk/gtk.h>
#include "callback.h"
#include "raccourcis.h"
GtkAccelGroup *accel_group_new (gpointer user_data)
{
  GtkAccelGroup *p_accel_group = NULL;
  p_accel_group = gtk_accel_group_new ();
  /* ... */
  return p_accel_group;
}

    Vous commencez à avoir l'habitude de cette organisation, nous allons donc créer une fonction qui va se charger d'ajouter un raccourci au groupe :

    raccourcis.c
    static void accelerator_new (GtkAccelGroup *p_accel_group, const gchar *accelerator, const gchar *accel_path,
                             GCallback callback, gpointer user_data)
{
  guint key;
  GdkModifierType mods;
  GClosure *closure = NULL;
  gtk_accelerator_parse (accelerator, &key, &mods);
  closure = g_cclosure_new (callback, user_data, NULL);
  gtk_accel_group_connect (p_accel_group, key, mods, GTK_ACCEL_VISIBLE, closure);
  gtk_accel_map_add_entry (accel_path, key, mods);
}

    La fonction gtk_accelerator_parse permet de décomposer une chaîne de caractères de la forme "<Control>F1" ou encore "<Alt>A" en numéro de touche et modificateur.
    En plus des touches, pour créer un raccourci clavier, nous avons besoin d'une fonction à appeler lorsque l'utilisateur appuie sur les touches spécifiées. gtk_accel_group_connect attend une fonction du type GClosure, regardons la documentation de gobject pour savoir à quoi cela correspond :

    typedef struct {
} GClosure;

    Bon pas très instructif :( Heureusement en regardant le constructeur de la classe GClosure on retombe sur des choses connues :

    GClosure *g_cclosure_new (GCallback callback_func, gpointer user_data, GClosureNotify destroy_data);

    Le dernier paramètre est une fonction qui sera appelée pour détruire l'objet user_data lorsqu'il ne sera plus utilisé.
    Voilà nous pouvons déjà connecter le raccourci clavier à notre fonction callback.
    Pour finir, pour associer un élément du menu à un raccourci clavier, nous avons besoin de l'ajouter à la carte des raccourcis, cette carte est unique et spécifique à chaque application :

    void gtk_accel_map_add_entry (const gchar *accel_path, guint accel_key, GdkModifierType accel_mods);

    Il nous manque juste le paramètre accel_path. Il s'agit, comme son nom le laisse penser, d'un chemin pour notre raccourci. Ce chemin est semblable à un chemin de fichier : "<WINDOWTYPE>/Category1/Category2/.../Action" où WINDOWTYPE est un identifiant spécifique à chaque application, pour notre application, nous utiliserons EditeurGTK, ensuite la documentation de GTK+ conseille, pour les éléments du menu, d'utiliser son chemin, par exemple pour l'élément Nouveau : "Fichier/Nouveau" ce qui nous donne : "<EditeurGTK>/Fichier/Nouveau". Comme il va être nécessaire de reprendre ces chemins lors de la création des éléments du menu, il est préférable d'en faire des constantes :

    raccourcis.h
    #define ACCEL_PATH_NEW "<EditeurGTK>/Fichier/Nouveau"
#define ACCEL_PATH_OPEN "<EditeurGTK>/Fichier/Ouvrir"
#define ACCEL_PATH_SAVE "<EditeurGTK>/Fichier/Enregistrer"
#define ACCEL_PATH_SAVEAS "<EditeurGTK>/Fichier/Enregistrer sous"
#define ACCEL_PATH_CLOSE "<EditeurGTK>/Fichier/Fermer"
#define ACCEL_PATH_QUIT "<EditeurGTK>/Fichier/Quitter"

    Avant de créer nos raccourcis, il faut régler un problème (sur ce point je trouve que GTK+ est mal fait), en effet il est précisé que la fonction callback pour les raccourcis doit avoir la signature suivante :

    gboolean (*GtkAccelGroupActivate) (GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier);

    Alors que nous avons des fonctions de la forme :

    void callback (GtkWidget *p_widget, gpointer user_data);

    On est donc obligé de créer des fonctions de type GtkAccelGroupActivate qui vont se charger d'appeler nos fonctions callback :

    raccourcis.c
    static gboolean accel_new (GtkAccelGroup *accel_group, GObject *acceleratable, guint keyval, GdkModifierType modifier, gpointer user_data)
{
  cb_new (NULL, user_data);
  return TRUE;
}

    Notre fonction n'a pas la même signature que les GtkAccelGroupActivate puisque g_cclosure_new précise qu'il appelle la fonction callback ainsi créée avec user_data comme dernier paramètre.

    Maintenant que le problème est résolu, créons nos raccourcis :

    raccourcis.c
      accelerator_new (p_accel_group, "<Control>N", ACCEL_PATH_NEW, G_CALLBACK (accel_new), user_data);
  accelerator_new (p_accel_group, "<Control>O", ACCEL_PATH_OPEN, G_CALLBACK (accel_open), user_data);
  accelerator_new (p_accel_group, "<Control>S", ACCEL_PATH_SAVE, G_CALLBACK (accel_save), user_data);
  accelerator_new (p_accel_group, "<Control><Shift>S", ACCEL_PATH_SAVEAS, G_CALLBACK (accel_saveas), user_data);
  accelerator_new (p_accel_group, "<Control>W", ACCEL_PATH_CLOSE, G_CALLBACK (accel_close), user_data);
  accelerator_new (p_accel_group, "<Control>Q", ACCEL_PATH_QUIT, G_CALLBACK (accel_quit), user_data);

    Pour finir, il faut ajouter le GtkAccelGroup à notre fenêtre principale :

    raccourcis.c
      gtk_window_add_accel_group (docs.p_main_window, p_accel_group);

    Voilà, nous en avons fini avec ce fichier, maintenant il faut revenir à menu.c pour associer les raccouris aux éléments du menu. Il nous suffit de modifier notre constructeur de GtkMenuItem pour qu'il prenne en argument le accel_path :

    menu.c
    static void menu_item_new (GtkMenu *p_menu, const gchar *title, const gchar *accel_path, GCallback callback, gpointer user_data)
{
  /* ... */
  gtk_menu_item_set_accel_path (GTK_MENU_ITEM (p_menu_item), accel_path);
}

    Et d'utiliser nos constantes lors de l'appel à la fonction meni_item_new :

    menu.c
        menu_item_new (GTK_MENU (p_menu), "_Nouveau", ACCEL_PATH_NEW, G_CALLBACK (cb_new), user_data);


GTK+ par l'exemple

Table des matières

  • XV. Messages d'erreur
    • XV-A. Aperçu
    • XV-B. Amélioration de nos fonctions d'affichage d'erreur

    XV. Messages d'erreur

    XV-A. Aperçu

    Image non disponible

    XV-B. Amélioration de nos fonctions d'affichage d'erreur

    Jusqu'à présent les messages d'erreurs étaient affichés à l'aide de la fonction printf mais cela oblige l'utilisateur à lancer le programme dans une console, pas très conviviale. Encore une fois GTK+ vient à notre secours en proposant une classe GtkMessageDialog qui nous permet d'afficher un message simplement, sans avoir à créer une boîte de dialogue en partant de zéro.
    Voici la fonction qui nous permet de créer notre boîte de dialogue :

    GtkWidget *gtk_message_dialog_new (GtkWindow *parent, GtkDialogFlags flags, GtkMessageType type, GtkButtonsType buttons,
                                   const gchar *message_format, ...);

    Donc nous avons besoin de notre fenêtre principale, pour cela il suffit d'inclure document.h, comme l'utilisateur doit d'abord valider notre message avant de continuer à utiliser l'éditeur, nous allons la rendre modale grâce à la constante GTK_DIALOG_MODAL. Ensuite vient le type de message, cela va dépendre de la fonction appelée (GTK_MESSAGE_INFO, GTK_MESSAGE_WARNING ou GTK_MESSAGE_ERROR). Le seul bouton dont l'utilisateur a besoin est le bouton Ok pour fermer la boîte de dialogue (GTK_BUTTONS_OK) et pour finir, la fonction attend le message à afficher (sous la même forme que la fonction printf).
    Comme seul le type de message et le message va changer selon la fonction print_* appelée, une nouvelle fonction est la bienvenue :

    error.c
    static void print_message (GtkMessageType type, const gchar *format, va_list va)
{
  gchar *message = NULL;
  GtkWidget *p_dialog = NULL;
  message = g_strdup_vprintf (format, va);
  p_dialog = gtk_message_dialog_new (docs.p_main_window, GTK_DIALOG_MODAL, type, GTK_BUTTONS_OK, message);
  g_free (message), message = NULL;
  gtk_dialog_run (GTK_DIALOG (p_dialog));
  gtk_widget_destroy (p_dialog);
}

    Les fonctions de la famille de g_strdup_printf sont extrêmement intéressantes puisqu'elles permettent de créer une nouvelle chaîne de caractères en utilisant la puissance des fonctions de la famille de printf (Voici un exemple d'implémentation de ce genre de fonction : Créer une chaîne de caractères formatée).
    Il nous reste plus qu'à modifier nos trois fonctions d'affichage de message, voici l'exemple pour print_info :

    error.c
    void print_info (char *format, ...)
{
  va_list va;
  va_start (va, format);
  print_message (GTK_MESSAGE_INFO, format, va);
}

    En C99, avec les macro à nombres variables, nous pourrions remplacer nos fonctions par des macro :

    #define print_info(chat *format, ...) print_message (GTK_MESSAGE_INFO, (format), __VA_ARGS__)


GTK+ par l'exemple

Table des matières

  • XVI. Ouvrir plusieurs fichiers en même temps
    • XVI-A. Aperçu
    • XVI-B. Mise en place des onglets
    • XVI-C. Changement de page
    • XVI-D. Fermer un onglet
    • XVI-E. Fermer tous les onglets
    • XVI-F. Modifier le titre de la page

XVI. Ouvrir plusieurs fichiers en même temps

XVI-A. Aperçu

Image non disponible

XVI-B. Mise en place des onglets

Actuellement, nous ne pouvons ouvrir qu'un seul fichier à la fois. Les éditeurs de texte avancés proposent généralement la possibilité d'ouvrir plusieurs documents simultanément grâce à un système d'onglets. C'est ce que nous allons mettre en place grâce au widget GtkNotebook.
A la place du GtkTextView, nous créons un GtkNotebook et comme nous allons avoir besoin de modifier de widget (ajout/suppression de pages), nous gardons un pointeur dessus dans notre structure globale :

main.c
  /* Creation de la page d'onglets */
  {
    GtkWidget *p_notebook = NULL;
    p_notebook = gtk_notebook_new ();
    gtk_container_add (GTK_CONTAINER (p_main_box), p_notebook);
    /* ... */
    docs.p_notebook = GTK_NOTEBOOK (p_notebook);
  }

Donc à l'ouverture de l'éditeur, aucun onglet est ouvert, ils seront ajoutés lorsque l'utilisateur crée un document ou en ouvre un. Par chance (ou grâce à l'ingéniosité de l'auteur de ce document, je vous laisse choisir :D), l'ajout d'une nouvelle page se fait uniquement dans la fonction cb_new. Nous avons anticipé la mise en place d'onglet au début de ce tutoriel en créant la structure docs_t dont seul le champs actif était utilisé, maintenant, il faut ajouter chaque document ouvert à la GList :

callback.c
void cb_new (GtkWidget *p_widget, gpointer user_data)
{
  document_t *nouveau = NULL;
  nouveau = g_malloc (sizeof (*nouveau));
  nouveau->chemin = NULL;
  /* Le document vient d'etre ouvert, il n'est donc pas modifie */
  nouveau->sauve = TRUE;
  docs.tous = g_list_append (docs.tous, nouveau);
  {
    gint index = 0;
    GtkWidget *p_scrolled_window = NULL;
    p_scrolled_window = gtk_scrolled_window_new (NULL, NULL);
    gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (p_scrolled_window), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
    nouveau->p_text_view = GTK_TEXT_VIEW (gtk_text_view_new ());
    {
      GtkTextBuffer *p_buffer = NULL;
      p_buffer = gtk_text_view_get_buffer (nouveau->p_text_view);
      g_signal_connect (G_OBJECT (p_buffer), "changed", G_CALLBACK (cb_modifie), NULL);
    }
    gtk_container_add (GTK_CONTAINER (p_scrolled_window), GTK_WIDGET (nouveau->p_text_view));
    index = gtk_notebook_append_page (docs.p_notebook, p_scrolled_window, GTK_WIDGET (gtk_label_new ("Nouveau document")));
    gtk_widget_show_all (p_scrolled_window);
    gtk_notebook_set_current_page (docs.p_notebook, index);
  }
  /* parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

Première chose intéressante, il n'est plus nécessaire de fermer le document pour en ouvrir un autre. Ensuite, plutôt que de modifier le champ actif, on crée un nouveau document que l'on ajoute à la GList. Le GtkTextView est créé comme précédemment mis à part qu'il est ajouté à un nouvel onglet que l'on crée grâce à la fonction :

gint gtk_notebook_append_page (GtkNotebook *notebook, GtkWidget *child, GtkWidget *tab_label);

Qui a besoin du widget enfant (il s'agit du GtkScrolledWindow contenant le GtkTextView) et d'un second widget qui sera affiché dans l'onglet (nous laissons GTK+ mettre un texte par défaut, nous verrons plus loin comment améliorer cela).
Une fois l'onglet créé, gtk_notebook_append_page nous retourne la position de la nouvelle page créée qui va nous servir à rendre la page active grâce à la fonction :

void gtk_notebook_set_current_page (GtkNotebook *notebook, gint page_num);

XVI-C. Changement de page

Pour pouvoir mettre à jour le document actif lorsque l'utilisateur navigue entre les différents onglets, il suffit d'intercepter le signal switch-page :

main.c
g_signal_connect (G_OBJECT (p_notebook), "switch-page", G_CALLBACK (cb_page_change), NULL);

La fonction cb_page_change récupére la position de la page courante et fait pointer actif vers la structure document_t correspondante stockée dans la GList :

callback.c
void cb_page_change (GtkNotebook *notebook, GtkNotebookPage *page, guint page_num, gpointer user_data)
{
  docs.actif = g_list_nth_data (docs.tous, page_num);
  /* parametres inutilises */
  (void)notebook;
  (void)user_data;
}

Comme GTK+ fait bien les choses, la fonction gtk_notebook_set_current_page que nous appelons lors de la création d'un document émet ce signal, donc pas besoin de recopier ce code dans cb_new.

XVI-D. Fermer un onglet

La dernière fonction qui nécessite quelques modifications, est cb_close, il faut maintenant supprimer le document de la GList, libérer la mémoire et supprimer l'onglet :

callback.c
void cb_close (GtkWidget *p_widget, gpointer user_data)
{
  /* Avant de fermer, il faut verifier qu'un document a bien ete ouvert */
  if (docs.actif)
  {
    /* ... */
    {
      docs.tous = g_list_remove (docs.tous, docs.actif);
      g_free (docs.actif->chemin), docs.actif->chemin = NULL;
      g_free (docs.actif), docs.actif = NULL;
      gtk_notebook_remove_page (docs.p_notebook, gtk_notebook_get_current_page (docs.p_notebook));
      if (gtk_notebook_get_n_pages (docs.p_notebook) > 0)
      {
        docs.actif = g_list_nth_data (docs.tous, gtk_notebook_get_current_page (docs.p_notebook));
      }
      else
      {
        docs.actif = NULL;
      }
    }
  }
  else
  {
    print_warning ("Aucun document ouvert");
  }
  /* parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

Il reste plus qu'à supprimer l'appel à la fonction gtk_widget_set_sensitive dans la fonction open_file, maintenant devenu inutile.

Bizarrement la suppression d'un onglet n'émet pas de signal switch-page, nous devons le faire manuellement.

XVI-E. Fermer tous les onglets

Maintenant que nous pouvons ouvrir plusieurs documents en même temps, il est nécessaire de les fermer tous lorsqu'on quitte le programme.
J'ai essayé plusieurs méthodes avant de trouver une méthode convenable. Contrairement à ce que l'on pourrait penser, il ne suffit pas d'utiliser la fonction g_list_foreach, qui permet de parcourir l'ensemble d'une liste chaînée, et de fermer les documents un à un. Le problème vient des documents non sauvegardés, puisque l'utilisateur peut à tout moment décider d'annuler l'enregistrement d'un document ce qui nous oblige à annuler la fermeture de l'application.
Le principe que j'ai choisi d'adopter consiste à boucler tant que tous les documents ne sont pas fermés (dans ce cas docs.actif vaut NULL) et de demander la fermeture du premier onglet (puisque le numéro du dernier change à chaque itération). Si l'utilisateur choisit d'annuler l'enregistrement, dans ce cas l'onglet n'est pas fermé et le nombre de documents ouverts est le même avant et après l'appel à cb_close, nous mettons alors fin à la boucle et signalons l'annulation en retournant FALSE, si tous les documents ont bien été fermés, la fonction renvoit TRUE :

static gboolean close_all (void)
{
  gboolean ret = TRUE;
  while (docs.actif)
  {
    gint tmp = gtk_notebook_get_n_pages (docs.p_notebook);
    gtk_notebook_set_current_page (docs.p_notebook, 0);
    cb_close (NULL, NULL);
    if (gtk_notebook_get_n_pages (docs.p_notebook) >= tmp)
    {
      ret = FALSE;
      break;
    }
  }
  return ret;
}

Et la fonction cb_quit devient :

void cb_quit (GtkWidget *p_widget, gpointer user_data)
{
  if (close_all ())
  {
    g_free (docs.dir_name), docs.dir_name = NULL;
    gtk_main_quit();
  }
  /* parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

XVI-F. Modifier le titre de la page

Pour plus d'esthétisme, nous allons modifier les titres des onglets. Pour les nouveaux documents, nous afficherons un texte par défaut (Nouveau document par exemple), une fois enregistré nous afficherons le nom du fichier (sans le chemin pour faire court). Et lorsque le document a été modifié depuis la dernière sauvegarde, nous ajouterons un petit astérisque à côté du titre.
Les bases étant posées, nous allons commencer par construire notre titre :

callback.c
static void set_title (void)
{
  if (docs.actif)
  {
    gchar *title = NULL;
    gchar *tmp = NULL;
    if (docs.actif->chemin)
    {
      tmp = g_path_get_basename (docs.actif->chemin);
    }
    else
    {
      tmp = g_strdup ("Nouveau document");
    }
    if (docs.actif->sauve)
    {
      title = g_strdup (tmp);
    }
    else
    {
      title = g_strdup_printf ("%s *", tmp);
    }
    g_free (tmp), tmp = NULL;
    /* ... */
    g_free (title), title = NULL;
  }
}

Quelques lignes de code simplifiées grâce aux fonctions de la glib. Nous obtenons donc notre titre dans la variable titre qui nous reste plus qu'à insérer dans l'onglet :

callback.c
    {
      gint index = 0;
      GtkWidget *p_child = NULL;
      GtkLabel *p_label = NULL;
      index = gtk_notebook_get_current_page (docs.p_notebook);
      p_child = gtk_notebook_get_nth_page (docs.p_notebook, index);
      p_label = GTK_LABEL (gtk_notebook_get_tab_label (docs.p_notebook, p_child));
      gtk_label_set_text (p_label, title);
    }

Cela peut paraître bizarre mais modifier le titre d'un onglet n'est pas trivial, il faut commencer par récupérer le numéro de la page en cours pour obtenir le widget qui est affiché dans l'onglet (car nous sommes libre de mettre le widget de notre choix), et comme dans notre cas, c'est un simple GtkLabel, on utilise la fonction gtk_label_set_text pour mettre à jour le titre. Pour finir, il faut faire appel à la fonction set_title lorsqu'on crée, ouvre, sauvegarde ou modifie un document, c'est à dire respectivement dans les fonctions cb_new, open_file, cb_save et cb_modifie.
Et voilà, c'est tout ! Moyennant quelques efforts de réflexion au début, le changement entre un éditeur simple et multi-documents est extrêmement simple et a même simplifié notre code : par exemple, pour la création du menu, nous n'avons plus besoin du paramètre user_data (pour simplifier, et au cas où nous en aurions de nouveau besoin, nous passons la valeur NULL).


GTK+ par l'exemple

Table des matières

  • XVII. Afficher l'arborescence du disque
    • XVII-A. Aperçu
    • XVII-B. Préparons le terrain
    • XVII-C. Création d'un GtkTreeView
      • XVII-C-1. Création du magasin
      • XVII-C-2. Affichage de l'arborescence
      • XVII-C-3. Sélectionner un fichier

XVII. Afficher l'arborescence du disque

XVII-A. Aperçu

Image non disponible

XVII-B. Préparons le terrain

Nous allons avoir besoin d'ajouter un GtkTreeView à côté du GtkTextView. La première idée qui vient à l'esprit est de créer un GtkHBox dans la boîte principale. Mais pour varier les plaisirs, nous allons utiliser un nouveau type de conteneur : les GtkPaned, ils permettent de séparer une zone en deux parties séparées par une barre mobile.
Nous créons donc un GtkHPaned (la séparation se fait dans le sens horizontal, à l'opposé des GtkVPaned) :

main.c
  GtkWidget *p_hpaned = NULL;
  /* ... */
  p_hpaned = gtk_hpaned_new ();
  gtk_box_pack_start (GTK_BOX (p_main_box), p_hpaned, TRUE, TRUE, 0);

Et plutôt que d'afficher la GtkNotebook dans la boîte principale, nous l'affichons maintenant dans la seconde partie de notre nouveau containeur :

main.c
    gtk_paned_add2 (GTK_PANED (p_hpaned), p_notebook);

XVII-C. Création d'un GtkTreeView

Les GtkTreeView sont sûrement les widgets les plus difficiles à maîtriser. Ceci est due au fait que la gestion du contenu et de l'affichage est séparé en deux widgets et qu'il est possible d'afficher une simple liste ou un arbre :

  • GtkListStore et GtkTreeStore pour stocker respectivement, le contenu d'une liste et d'un arbre
  • GtkTreeView pour afficher le contenu des Gtk*Store.

Nous avons donc le choix entre afficher le contenu du répertoire courant ou afficher toute l'arborescence du disque. Nous opterons pour la première solution car lister tout le contenu du disque serait bien trop long (surtout de nos jours où un disque dur fait plusieurs dizaines de GO). Mais pour ne pas se limiter au répertoire où se trouve notre programme (ce qui serait gênant s'il se trouve dans /usr/bin), nous allons aussi lister les répertoires présents pour permettre à l'utilisateur de naviguer dans l'arborescence.
Pour résumer nos objectifs : nous voulons créer un GtkTreeView qui affichera un GtkListStore contenant le nom des fichiers et dossiers présents dans un répertoire donné. Lorsque l'utilisateur clique sur un nom représentant un fichier, on l'ouvre, s'il s'agit d'un dossier, on réactualise le GtkListStore avec le contenu de ce nouveau répertoire.
Y a plus qu'a...

XVII-C-1. Création du magasin

main.c
    p_list_store = gtk_list_store_new (2, GDK_TYPE_PIXBUF, G_TYPE_STRING);
    docs.p_list_store = p_list_store;
    docs.dir_name = g_strdup (g_get_home_dir ());
    dir_list ();

Qui a osé dire qu'il s'agissait de la partie la plus difficile ? Vous l'aurez compris, tout le code se trouve dans la fonction dir_list que nous verrons plus tard. Pour commencer, on construit notre GtkListStore en précisant le nombre de colonnes souhaité suivi de leurs types. Nous souhaitons deux colonnes, la première contiendra un GdkPixbuf (une image) pour différencier les dossiers des fichiers, et la seconde le nom du fichier.
Ensuite, nous sauvegardons notre GtkListStrore et le chemin du dossier à afficher (la fonction g_get_home_dir nous permet de connaître le dossier de l'utilisateur) dans notre structure globale car nous en avons besoin dans plusieurs fonctions callback par la suite.
Maintenant passons au remplissage du magasin. Comme nous serons amenés à rafraîchir le contenu de ce dernier, on commence par le vider :

callback.c
    gtk_list_store_clear (docs.p_list_store);

Pour lister le contenu du répertoire, nous allons utiliser la glib. Après avoir ouvert le répertoire, il nous suffit d'appeler la fonction g_dir_read_name qui à chaque appel nous renvoie le fichier (9) suivant puis NULL lorsque tout le répertoire a été parcouru :

callback.c
void dir_list (void)
{
  GDir *dir = NULL;
  dir = g_dir_open (docs.dir_name, 0, NULL);
  if (dir)
  {
    const gchar *read_name = NULL;
    /* ... */
    while ((read_name = g_dir_read_name (dir)))
    {
      /* ... */
    }
    g_dir_close (dir), dir = NULL;
  }
}

Pour chaque fichier, il faut commencer par reconstruire son chemin complet :

callback.c
      gchar *file_name = NULL;
      file_name = g_build_path (G_DIR_SEPARATOR_S, docs.dir_name, read_name, NULL);

La fonction g_build_path concatène l'ensemble de ses arguments, sauf le premier qui est le séparateur utilisé. Maintenant que nous avons notre nom complet, nous pouvons tester si notre fichiers est un dossier ou pas :

callback.c
    GdkPixbuf *p_file_image = NULL;
      /* ... */
      if (g_file_test (file_name, G_FILE_TEST_IS_DIR))
      {
        p_file_image = gdk_pixbuf_new_from_file ("dossier.png", NULL);
      }
      else
      {
        p_file_image = gdk_pixbuf_new_from_file ("fichier.png", NULL);
      }

La fonction permet de tester différentes propriétés relatives aux fichiers :

typedef enum
{
  G_FILE_TEST_IS_REGULAR    = 1 << 0, /* TRUE si le fichier est un fichier standard (ni un lien symbolique ni un dossier) */
  G_FILE_TEST_IS_SYMLINK    = 1 << 1, /* TRUE si le fichier est un lien symbolique */
  G_FILE_TEST_IS_DIR        = 1 << 2, /* TRUE si le fichier est un dossier */
  G_FILE_TEST_IS_EXECUTABLE = 1 << 3, /* TRUE si le fichier est un executable */
  G_FILE_TEST_EXISTS        = 1 << 4  /* TRUE si le fichier existe */
} GFileTest;

Donc selon le type de fichier, nous chargeons l'image approprié dans un GdkPixbuf. Le second paramètre de la fonction gdk_pixbuf_new_from_file permet de récupérer plus d'information lorsqu'une erreur survient.
Pour finir, il nous reste plus qu'à ajouter une nouvelle colonne dans le GtkListStore. Ceci se réalise en deux étapes. La première consiste à ajouter une colonne :

callback.c
    GtkTreeIter iter;
/* ... */
      gtk_list_store_append (docs.p_list_store, &iter);

Comme pour le GtkTextView, nous avons besoin d'un itérateur qui va nous permettre de remplir cette colonne à l'aide d'une seconde fonction :

callback.c
      gtk_list_store_set (docs.p_list_store, &iter, 0, p_file_image, 1, read_name, -1);

Le premier paramètre est bien sûr notre magasin, ensuite il s'agit de l'itérateur symbolisant la colonne nouvellement créée et enfin des couples numéro de colonne/donnée qui doivent correspondre au nombre et type précisé lors de la création du GkListStore.
En plus de cela, nous ajoutons aussi un dossier "..", puisque la fonction g_dir_read_name ne le liste pas, pour permettre à l'utilisateur de remonter dans l'arborescence.

XVII-C-2. Affichage de l'arborescence

Voilà notre GtkListStore est prêt ! Maintenant il nous faut associer ce dernier au GtkTreeView. Ceci n'a besoin d'être fait qu'une seule fois lors de la création du widget :

main.c
    p_tree_view = gtk_tree_view_new_with_model (GTK_TREE_MODEL (p_list_store));

GtkTreeModel est une interface utilisée par GtkTreeView, la classe GtkListStore implémentant cette interface, il est possible de réaliser un transtypage.
Si l'histoire s'arrêterai là, créer un GtkTextView sera plutôt simple. Hélas nous devons créer les colonnes dans le GtkTextView et les faire correspondre à celles du magasin.
Le nombre d'éléments affichables par une cellule d'un GtkTreeView étant varié, il faut commencer par créer un GtkCellRenderer qui peut être de différents types :

  • GtkCellRendererText : pour afficher du texte
  • GtkCellRendererPixbuf : pour les images
  • GtkCellRendererProgress : permet d'ajouter une barre de progression
  • GtkCellRendererToggle : affiche une case à cocher.

Dans notre cas, nous avons besoin que des deux premiers. Voici le code pour la première colonne qui contiendra l'image :

main.c
    GtkCellRenderer *p_renderer = NULL;
/* ... */
    p_renderer = gtk_cell_renderer_pixbuf_new ();

Une fois la cellule créée, il faut créer la colonne à proprement parlée grâce à la fonction :

GtkTreeViewColumn* gtk_tree_view_column_new_with_attributes
                                            (const gchar *title,
                                             GtkCellRenderer *cell,
                                             ...);

Après le titre de la colonne et le GtkCellRenderer, la fonction attend une chaîne de caractères qui diffère selon le type de GtkCellRenderer suivi du numéro de la colonne. Comme il est possible de passer plusieurs couples identifiant/numéro de colonne, nous terminons la liste par NULL.
Et pour terminer, on ajoute la colonne au GtkTextView :

main.c
    gtk_tree_view_append_column (GTK_TREE_VIEW (p_tree_view), p_column);

La démarche est identique pour la seconde colonne :

main.c
    p_renderer = gtk_cell_renderer_text_new ();
    p_column = gtk_tree_view_column_new_with_attributes (NULL, p_renderer, "text", 1, NULL);
    gtk_tree_view_append_column (GTK_TREE_VIEW (p_tree_view), p_column);

Et comme nous n'avons pas besoin des en-têtes de colonnes, nous les cachons :

main.c
    gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (p_tree_view), FALSE);

Je suis passé rapidement sur cette partie car la documentation officielle n'est pas très complète à ce sujet et le rôle des fonctions n'est pas très claire.

XVII-C-3. Sélectionner un fichier

Nous arrivons à afficher le contenu d'un dossier, il nous reste plus qu'à réagir à la sélection d'une colonne. Vous commencez à être habitué, il faut intercepter le signal row-activated du GtkTextView :

main.c
    g_signal_connect (G_OBJECT (p_tree_view), "row-activated", G_CALLBACK (cb_select), NULL);

La fonction callback correspondante est différente de ce qu'on a l'habitude de voir :

void cb_select (GtkTreeView *p_tree_view, GtkTreePath *arg1, GtkTreeViewColumn *arg2, gpointer user_data)
{
  /* ... */
}

Ces arguments vont nous permettrent de retrouver la cellule sélectionnée. La méthode est comparable à celle que l'on utilise pour les GtkTexView, on commence par récupérer notre magasin sous forme de GtkTreeModel comme nous pourrions le faire avec un GtkTextBuffer :

  GtkTreeModel *p_tree_model = NULL;
  p_tree_model = gtk_tree_view_get_model (p_tree_view);

Ensuite à l'aide du GtkTreePath, qui est une représentation du chemin pour accéder à l'élément sélectionné, nous récupérons un GtkTreeIter :

  GtkTreeIter iter;
  /* ... */
  gtk_tree_model_get_iter (p_tree_model, &iter, arg1);

Et pour finir, on récupère le contenu de la seconde colonne grâce au GtkTreeIter :

  gchar *str = NULL;
  /* ... */
  gtk_tree_model_get (p_tree_model, &iter, 1, &str, -1);

Nous pourrions récupérer plusieurs colonnes en même temps en ajoutant d'autre couple numéro de colonne/pointeur sur un type de variable compatible avec ce que nous avons stocké dans le GtkListStore.
Voilà c'est la fin de la partie floue (je m'en excuse mais la documentation n'est pas très complète à se sujet, il faut donc faire des essais en tâtonnant jusqu'à se que cela fonctionne sans forcément en connaître les raisons :().
Maintenant que nous avons notre nom de fichier, il faut retrouver son chemin complet et tester s'il s'agit d'un dossier ou non. Ceci a déjà été vu lors de la création du GtkListStore :

  gchar *file_name = NULL;
  /* ... */
  file_name = g_build_path (G_DIR_SEPARATOR_S, docs.dir_name, str, NULL);
  g_free (str), str = NULL;
  if (g_file_test (file_name, G_FILE_TEST_IS_DIR))
  {
    /* ... */
  }
  else
  {
    /* ... */
  }

Commençons par le plus difficile : dans le cas où notre fichier est un dossier, il suffit de remplacer le nom du dossier à afficher et de rafraîchir le GtkListStore en faisant appel à la fonction dir_list :

    g_free (docs.dir_name), docs.dir_name = NULL;
    docs.dir_name = file_name;
    dir_list ();

Difficile de faire plus simple ! Pour ouvrir un fichier, il suffit de faire appel à la fonction open_file :

    open_file (file_name);


GTK+ par l'exemple

Table des matières

  • XVIII. Notre signature
    • XVIII-A. Aperçu
    • XVIII-B. Boîte A propos

XVIII. Notre signature

XVIII-A. Aperçu

Image non disponible

XVIII-B. Boîte A propos

Nous allons terminer cette initiation par un petit ajout qui va finir notre application : une boîte de dialogue A propos. Depuis la version 2.6 de GTK+, il existe un widget qui va nous simplifier la vie : GtkAboutDialog.
Son utilisation est plutôt simple, il suffit de créer le widget, puis un certain nombre de fonctions permettent de renseigner les informations concernant le programme (auteur, version licence...) puis on affiche la boîte de dialogue :

Sélectionnez
void cb_about (GtkWidget *p_widget, gpointer user_data)
{
  GtkWidget *p_about_dialog = NULL;
  p_about_dialog = gtk_about_dialog_new ();
  gtk_about_dialog_set_version (GTK_ABOUT_DIALOG (p_about_dialog), "1.0");
  gtk_about_dialog_set_name (GTK_ABOUT_DIALOG (p_about_dialog), "Editeur de texte");

  {
    const gchar *authors[2] = {"gege2061", NULL};
    gtk_about_dialog_set_authors (GTK_ABOUT_DIALOG (p_about_dialog), authors);
  }
  {
    gchar *contents = NULL;
    if (g_file_get_contents ("COPYING", &contents, NULL, NULL))
    {
      gchar *utf8 = NULL;
      utf8 = g_locale_to_utf8 (contents, -1, NULL, NULL, NULL);
      g_free (contents), contents = NULL;
      gtk_about_dialog_set_license (GTK_ABOUT_DIALOG (p_about_dialog), utf8);
      g_free (utf8), utf8 = NULL;
    }
  }
  gtk_about_dialog_set_website (GTK_ABOUT_DIALOG (p_about_dialog), "http://nicolasj.developpez.com/");
  {
    GdkPixbuf *p_logo = NULL;
    p_logo = gdk_pixbuf_new_from_file ("logo.png", NULL);
    gtk_about_dialog_set_logo (GTK_ABOUT_DIALOG (p_about_dialog), p_logo);
  }
  gtk_dialog_run (GTK_DIALOG (p_about_dialog));
  /* parametres inutilises */
  (void)p_widget;
  (void)user_data;
}

Voilà rien de bien compliqué mais le résultat obtenu est plutôt sympathique.

Bizarrement pour la fonction gtk_about_dialog_set_authors le tableau doit être déclaré comme constant. Un tableau non constant provoque une erreur de compilation !


GTK+ par l'exemple

Table des matières

  • XIX. Conclusion
    • XIX-A. C'est déjà fini ?
    • XIX-B. Remerciements

XIX. Conclusion

XIX-A. C'est déjà fini ?

Eh oui c'est la fin de ce tutoriel ! Mais si vous avez pris autant de plaisir que moi à lire et surtout commencer à maîtriser la puissance de GTK+, alors ne vous inquiétez pas notre éditeur n'en est qu'a la version 1.0. Les prochaines versions ne sont pas prévues pour la correction des bugs (enfin j'espère), mais plutôt ajouter de nouvelles fonctionnalités :D Voici un aperçu des possibilités de GTK+ que je n'ai pas abordé ici mais qui pourront faire l'objet de tutoriels :

  • La création d'un écran de démarrage
  • Utilisation du widget GtkSourceView pour la coloration syntaxique
  • Le drag'n'drop
  • Une meilleure gestion des erreurs grâce au GError
  • Et plein d'autres choses que je ne connais pas encore...

Je vous l'ai déjà dit, mais je préfère le répéter, la documentation de l'API GTK+ est votre première source d'information, n'hésitez pas à passer quelques minutes à chercher une fonction avant de recréer la roue et surtout faites des essais, c'est comme cela que l'on apprend (j'ai découvert énormément de fonctionnalités en écrivant ce tutoriel).
Si malgré cela vous avez des difficultés lors de vos développements avec GTK+, les membres du forum C seront, j'en suis sûr, ravis de vous venir en aide ;)

XIX-B. Remerciements

Merci à loka, fearyourself et à Miles pour leur relecture attentive de cet article.

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2006-2008 Nicolas Joseph. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.